The Swiss Army Knife For Python Web Developers
Hinweis
Dies ist die deutsche Übersetzung des Tutorials. Die Entwicklung rund um Werkzeug steht nie still, und Verbesserungen an der Library wirken sich oft auch auf das Tutorial aus – deshalb ist die Originalversion möglicherweise aktueller.
Willkommen zum Tutorial für Werkzeug 0.2. Wir werden einen einfachen TinyURL-Klon programmieren, der die URLs in einer Datenbank speichert. Die Die verwendeten Bibliotheken für diese Anwendung sind Jinja für die Templates, SQLAlchemy für die Datenbank-Anbindung und natürlich Werkzeug für WSGI.
Wir haben uns hier für diese Komponenten entschieden, weil wir einen Django-ähnlichen Grundaufbau nachstellen wollen. Dazu zählen wir zum Beispiel View-Funktionen anstelle der in Rails und Pylons gängigen Controller-Klassen mit Action-Methoden, sowie designerfreundliche Templates.
In Werkzeugs Beispiel-Ordner befinden sich einige Anwendungen, die andere Konzepte verfolgen, Template-Engines einsetzen etc. Dort liegt auch der Quellcode der Anwendung, die wir in diesem Tutorial erstellen werden.
Du kannst easy_install verwenden, um Jinja und SQLAlchemy zu installieren, falls diese nicht bereits installiert sind:
sudo easy_install Jinja sudo easy_install SQLAlchemy
Diese Befehle funktionieren auch auf einem Windows-System (mit Administratorrechten), sofern die setuptools installiert sind, allerdings musst du das sudo weglassen. Als OS X-Benutzer könntest du die Libraries auch via port installieren, Linux-Benutzer finden diese Pakete möglicherweise auch in ihrem Paketmanager.
Wenn du neugierig bist, kannst du dir auch die Online-Demo der Anwendung ansehen.
Noch ein kleiner Hinweis: Dieses Tutorial erfordert Python 2.4.
Bevor wir beginnen können, müssen wir ein Python-Paket für unsere Werkzeug-Anwendung erstellen. Dort werden wir die Anwendung, die Templates und die statischen Dateien ablegen.
Die Anwendung dieses Tutorials nennen wir shorty und die Struktur für unsere Anwendung sieht etwa so aus:
manage.py shorty/ __init__.py static/ templates/
Die Dateien __init__.py und manage.py lassen wir für den Moment einmal leer. Die erste dieser Dateien macht aus dem Ordner shorty ein Python-Paket, die zweite werden wir später für unsere Verwaltungsfunktionen nutzen.
Im Gegensatz zu Django oder ähnlichen Frameworks arbeitet Werkzeug direkt auf der WSGI-Schicht. Es gibt keine schicke Magie, die die zentrale WSGI-Anwendung für uns implementiert. Das bedeutet, dass wir als Erstes eben diese programmieren müssen. Eine WSGI-Anwendung ist eine Funktion oder, noch besser, eine Klasse mit einer Methode __call__.
Eine aufrufbare Klasse hat große Vorteile gegenüber einer Funktion: Zum Einen kann man Konfigurationsparameter direkt an den Konstruktor übergeben, zum Anderen können wir WSGI-Middlewares innerhalb der WSGI-Anwendung hinzufügen. Das ist nützlich für Middlewares, die entscheidend für die Funktion der Anwendung sind (z.B. eine Session-Middleware).
Hier zunächst einmal der Quellcode für unsere Datei shorty/application.py, in der wir die WSGI-Anwendung ablegen:
from sqlalchemy import create_engine from werkzeug import Request, ClosingIterator from werkzeug.exceptions import HTTPException from shorty.utils import session, metadata, local, local_manager, url_map from shorty import views import shorty.models class Shorty(object): def __init__(self, db_uri): local.application = self self.database_engine = create_engine(db_uri, convert_unicode=True) def init_database(self): metadata.create_all(self.database_engine) def __call__(self, environ, start_response): local.application = self request = Request(environ) local.url_adapter = adapter = url_map.bind_to_environ(environ) try: endpoint, values = adapter.match() handler = getattr(views, endpoint) response = handler(request, **values) except HTTPException, e: response = e return ClosingIterator(response(environ, start_response), [session.remove, local_manager.cleanup])
Ziemlich viel für Code für den Anfang ... gehen wir ihn mal Schritt für Schritt durch. Zunächst sehen wir einige Imports: Aus dem Paket sqlalchemy holen wir uns eine Factory-Funktion, die eine neue Datenbank-Engine für uns erstellt, die wiederum einen Connection-Pool bereithält. Die nächsten Imports holen einige Objekte in den Namensraum, die uns Werkzeug zur Verfügung stellt: ein Request-Objekt; ein spezieller Iterator, der uns hilft am Ende eines Requests einige Dinge aufzuräumen; und schließlich die Basisklasse für alle HTTP-Exceptions.
Die nächsten fünf Imports funktionieren noch nicht, weil wir das utils-Modul noch nicht erstellt haben. Doch wir werden trotzdem schon ein wenig über diese Objekte sprechen. Das Objekt session ist kein aus PHP bekanntes Session-Array, sondern eine Datenbank-Session von SQLAlchemy. Alle Datenbank-Models, die im Kontext eines Requests erstellt werden, sind auf diesem Objekt zwischengespeichert, so dass man Änderungen mit einem Schlag zum Server senden kann. Im Gegensatz zu Django wird ein instantiiertes Datenbank-Model automatisch von der Session verwaltet und ist in dieser Session ein Singleton. Es kann also niemals zwei Instanzen des selben Datenbankeintrags in einer Session geben. Das Objekt metadata stammt ebenfalls aus SQLAlchemy und speichert Informationen über die Tabellen der Datenbank. Es stellt zum Beispiel eine Funktion bereit, die alle im Model definierten Tabellen in der Datenbank erstellt.
Das Objekt local ist ein kontext-lokales Objekt, erstellt vom Modul utility. Attributzugriffe auf dieses Objekt sind an den aktuellen Request gebunden, d.h. jeder Request bekommt ein anderes Objekt zurück und kann verschiedene Objekte ablegen, ohne Threadingprobleme zu bekommen. Der local_manager wird genutzt, um am Ende des Requests alle auf dem local-Objekt gespeicherten Daten wieder freizugeben.
Der letzte Import von dort ist die URL-Map, die alle URL-Routen verwaltet. Solltest du bereits mit Django gearbeitet haben, ist dies vergleichbar mit den regulären Ausdrücken in der jeweiligen urls.py. Kennst du PHP, ist die URL-Map ähnlich einem eingebauten mod_rewrite.
Zusätzlich importieren wir hier unser views-Modul, das die View-Funktionen enthält, sowie das models-Modul, in welchem unsere Models definiert sind. Auch wenn es so aussieht, als ob wir diesen Import nicht nutzen, ist er wichtig. Nur dadurch werden unsere Tabellen auf dem metadata-Objekt registriert.
Schauen wir auf die Anwendungsklasse. Der Konstruktor dieser Klasse nimmt die Datenbank-URI entgegen, die – einfach gesagt – den Typ der Datenbank und die Verbindungsdaten enthält. Für SQLite das wäre zum Beispiel sqlite:////tmp/shorty.db (die vier Slashes sind kein Tippfehler).
Im Konstruktor erstellen wir auch gleich eine Datenbank-Engine für diese URI und aktivieren das automatische Umwandeln von Bytestrings nach Unicode. Das ist nützlich, weil sowohl Jinja als auch Werkzeug intern nur Unicode verwenden.
Des Weiteren binden wir die Anwendung an das local-Objekt. Das ist eigentlich nicht nötig, aber nützlich, wenn wir mit der Anwendung in der Python-Shell spielen wollen. Damit werden direkt nach dem Instantiieren der Anwendung die Datenbankfunktionen testen. Wenn wir das nicht tun, wird der Python-Interpreter einen Fehler werfen, wenn außerhalb eines Requests versucht wird, eine SQLAlchemy-Session zu erstellen.
Die Methode init_database können wir später im Managementscript verwenden, um alle Tabellen zu erstellen, die wir definiert haben.
Nun zur eigentlichen WSGI-Anwendung, der __call__-Methode. Dort passiert das so genannte “Request Dispatching”, also das Weiterleiten von eingehenden Anfragen zu den richtigen Funktionen. Als Erstes erstellen wir dort ein neues Request-Objekt, um nicht direkt mit environ, dem Dictionary mit den Umgebungsvariablen, arbeiten zu müssen. Dann binden wir die Anwendung an das local-Objekt für den aktuellen Kontext.
Anschließend erstellen wir einen URL-Adapter, indem wir die URL-Map an die aktuelle WSGI-Umgebung binden. Der Adapter weiß dann, wie die aktuelle URL aussieht, wo die Anwendung eingebunden ist etc. Diesen Adapter können wir nutzen, um URLs zu erzeugen oder gegen den aktuellen Request zu matchen. Wir binden diesen Adapter auch an das local-Objekt, damit wir im utils-Modul auf ihn zugreifen können.
Danach kommt ein try/except-Konstrukt, das HTTP-Fehler abfängt, die während des Matchings oder in einer View-Funktion auftreten können. Wenn der Adapter keinen Endpoint für die aktuelle URL findet, wird er eine NotFound-Exception werfen, die wir wie ein Response Objekt aufrufen können. Der Endpoint ist in unserem Fall der Name der Funktion im views-Modul, die wir aufrufen möchten. Wir suchen uns einfach mit getattr die Funktion dem Namen nach heraus und rufen sie mit dem Request-Objekt und den URL-Werten auf.
Am Schluss rufen wir das gewonnene Response-Objekt (oder die Exception) als WSGI-Anwendung auf und übergeben den Rückgabewert dieser Funktion an den ClosingIterator, zusammen mit zwei Funktionen fürs Aufräumen. Dies schließt die SQLAlchemy-Session und leert das local-Objekt für diesen Request.
Nun müssen wir zwei leere Dateien shorty/views.py und shorty/models.py erstellen, damit die Imports nicht fehlschlagen. Den tatsächlichen Code für diese Module werden wir ein wenig später erstellen.
Nun haben wir die eigentliche WSGI-Applikation fertig gestellt, aber wir müssen das Utility-Modul noch um Code ergänzen, damit die Imports klappen. Fürs Erste fügen wir nur die Objekte hinzu, die wir brauchen, damit die Applikation funktioniert. Der folgende Code landet in der Datei shorty/utils.py:
from sqlalchemy import MetaData from sqlalchemy.orm import create_session, scoped_session from werkzeug import Local, LocalManager from werkzeug.routing import Map, Rule local = Local() local_manager = LocalManager([local]) application = local('application') metadata = MetaData() session = scoped_session(lambda: create_session(application.database_engine, transactional=True), local_manager.get_ident) url_map = Map() def expose(rule, **kw): def decorate(f): kw['endpoint'] = f.__name__ url_map.add(Rule(rule, **kw)) return f return decorate def url_for(endpoint, _external=False, **values): return local.url_adapter.build(endpoint, values, force_external=_external)
Zunächst importieren wir wieder eine Menge, dann erstellen wir das local-Objekt und den Manager dafür, wie bereits im vorherigen Abschnitt besprochen. Neu ist hier, dass der Aufruf eines local-Objekts mit einem String ein Proxy-Objekt zurück gibt. Dieses zeigt stets auf die gleichnamigen Attribute des local-Objekts. Beispielsweise verweist nun application dauerhaft auf local.application. Wenn du jedoch darauf zugreifst und kein Objekt an local.application gebunden ist, erhältst du einen RuntimeError.
Die folgenden drei Zeilen sind im Prinzip alles, um SQLAlchemy 0.4 oder höher in eine Werkzeug-Anwendung einzubinden. Wir erstellen ein Metadaten-Objekt für all unsere Tabellen sowie eine “scoped session” über die scoped_session-Factory-Funktion. Dadurch wird SQLAlchemy angewiesen, praktisch denselben Algorithmus zur Ermittlung des aktuellen Kontextes zu verwenden, wie es auch Werkzeug für die local-Objekte tut, und die Datenbank-Engine der aktuellen Applikation zu benutzen.
Wenn wir nicht vorhaben, mehrere Instanzen der Applikation in derselben Instanz des Python-Interpreters zu unterstützen, können wir den Code einfach halten, indem wir nicht über das aktuelle local-Objekt auf die Applikation zugreifen, sondern einen anderen Weg nehmen. Dieser Ansatz wird etwa von Django verfolgt, macht es allerdings unmöglich, mehrere solcher Applikationen zu kombinieren.
Der restliche Code des Moduls wird für unsere Views benutzt. Die Idee besteht darin, Dekoratoren zu benutzen, um die URL-Dispatching-Regeln für View-Funktionen festzulegen, anstatt ein zentrales Modul urls.py zu verwenden, wie es Django tut, oder über eine .htaccess-Datei URLs umzuschreiben, wie man es in PHP machen würde. Dies ist eine Möglichkeit, dies zu tun, und es gibt unzählige andere Wege der Handhabung von URL-Regeldefinitionen.
Die Funktion url_for, die wir ebenfalls definieren, bietet einen einfachen Weg, URLs anhand des Endpointes zu generieren. Wir werden sie später in den Views als auch unserem Model verwenden.
Da wir nun das Grundgerüst für unsere Anwendung fertig gestellt haben, können wir jetzt erst einmal relaxen und uns etwas komplett anderem zuwenden: den Verwaltungs-Scripts. Während der Entwicklung erledigt man häufig immer wiederkehrende Aufgaben, wie zum Beispiel das Starten eines Entwicklungs-Servers (im Gegensatz zu PHP benötigt Werkzeug keinen Apache-Server; der in Python integrierte wsgiref-Server ist völlig ausreichend und für die Entwicklung auf jeden Fall empfehlenswert), das Starten eines Python-Interpreters (um mit den Datenbankobjekten herumzuspielen oder die Datenbank zu initialisieren) etc.
Werkzeug macht es unglaublich einfach, solche Verwaltungs-Scripts zu schreiben. Der folgende Code implementiert ein voll funktionsfähiges Verwaltungs-Script und gehört in die manage.py-Datei, welche du am Anfang erstellt hast:
#!/usr/bin/env python from werkzeug import script def make_app(): from shorty.application import Shorty return Shorty('sqlite:////tmp/shorty.db') def make_shell(): from shorty import models, utils application = make_app() return locals() action_runserver = script.make_runserver(make_app, use_reloader=True) action_shell = script.make_shell(make_shell) action_initdb = lambda: make_app().init_database() script.run()
werkzeug.script ist genauer in der Script-Dokumentation beschrieben, und da der Großteil des Codes verständlich sein sollte, werden wir hier nicht näher darauf eingehen.
Es ist aber wichtig, dass du python manage.py shell ausführen kannst, um eine interaktive Python-Shell zu starten. Solltest du einen Traceback bekommen, kontrolliere bitte die darin genannte Code-Zeile und vergleiche sie mit dem entsprechenden Code in dieser Anleitung.
Sobald das Script läuft, können wir mit dem Schreiben der Datenbank-Models beginnen.
Jetzt können wir die Models erstellen. Da die Anwendung ziemlich einfach ist, haben wir nur ein Model und eine Tabelle:
from datetime import datetime from sqlalchemy import Table, Column, String, Boolean, DateTime from shorty.utils import session, metadata, url_for, get_random_uid url_table = Table('urls', metadata, Column('uid', String(140), primary_key=True), Column('target', String(500)), Column('added', DateTime), Column('public', Boolean) ) class URL(object): def __init__(self, target, public=True, uid=None, added=None): self.target = target self.public = public self.added = added or datetime.utcnow() if not uid: while True: uid = get_random_uid() if not URL.query.get(uid): break self.uid = uid @property def short_url(self): return url_for('link', uid=self.uid, _external=True) def __repr__(self): return '<URL %r>' % self.uid session.mapper(URL, url_table)
Dieses Modul ist gut überschaubar. Wir importieren alles, was wir von SQLAlchemy benötigen, und erstellen die Tabelle. Dann fügen wir eine Klasse für diese Tabelle hinzu und verbinden beide miteinander. Für eine detailliertere Erklärung bezüglich SQLAlchemy solltest du dir das exzellente Tutorial anschauen.
Im Konstruktor generieren wir solange eine eindeutige ID, bis wir eine finden, die noch nicht belegt ist. Die get_random_uid-Funktion fehlt – wir müssen sie noch in unser utils-Modul einfügen:
from random import sample, randrange URL_CHARS = 'abcdefghijkmpqrstuvwxyzABCDEFGHIJKLMNPQRST23456789' def get_random_uid(): return ''.join(sample(URL_CHARS, randrange(3, 9)))
Wenn das getan ist, können wir python manage.py initdb ausführen, um die Datenbank zu erstellen und python manage.py shell, um damit herumzuspielen:
Interactive Werkzeug Shell >>> from shorty.models import session, URL
Jetzt können wir einige URLs zu der Datenbank hinzufügen:
>>> urls = [URL('http://example.org/'), URL('http://localhost:5000/')] >>> URL.query.all() [] >>> session.commit() >>> URL.query.all() [<URL '5cFbsk'>, <URL 'mpugsT'>]
Wie du sehen kannst, müssen wir session.commit() aufrufen, um die Änderungen in der Datenbank zu speichern. Nun erstellen wir ein privates Element mit einer eigenen UID:
>>> URL('http://werkzeug.pocoo.org/', False, 'werkzeug-webpage') >>> session.commit()
Dann fragen wir alle ab:
>>> URL.query.filter_by(public=False).all() [<URL 'werkzeug-webpage'>] >>> URL.query.filter_by(public=True).all() [<URL '5cFbsk'>, <URL 'mpugsT'>] >>> URL.query.get('werkzeug-webpage') <URL 'werkzeug-webpage'>
Jetzt haben wir einige Datensätze in der Datenbank und wissen ungefähr, auf welche Weise SQLAlchemy funktioniert. Zeit, unsere Views zu erstellen.
Nachdem wir mit SQLAlchemy herumgespielt haben, können wir zurück zu Werkzeug gehen und anfangen, unsere View-Funktionen zu erstellen. Der Begriff “View-Funktion” kommt von Django. Dort werden die Funktionen, die Templates befüllen und ausgeben, so genannt. Deshalb ist unser Beispiel eine Umsetzung von MVT (Model, View, Template) und nicht etwa MVC (Model, View, Controller). Die beiden Bezeichnungen bedeuten dasselbe, aber es ist viel einfacher, dieselbe Benennung wie Django zu nutzen.
Als Anfang erstellen wir einfach eine View-Funktion für neue URLs und eine Funktion, die eine Nachricht über einen neuen Link darstellt. Das wird der Inhalt unserer noch leeren views.py-Datei:
from werkzeug import redirect from werkzeug.exceptions import NotFound from shorty.utils import session, render_template, expose, validate_url, \ url_for from shorty.models import URL @expose('/') def new(request): error = url = '' if request.method == 'POST': url = request.form.get('url') alias = request.form.get('alias') if not validate_url(url): error = u"Entschuldigung, aber ich kann die angegebene " \ u"URL nicht kürzen." elif alias: if len(alias) > 140: error = 'Dein Alias ist zu lang' elif '/' in alias: error = 'Dein Alias darf keinen Slash beinhalten' elif URL.query.get(alias): error = 'Der angegeben Alias existiert bereits' if not error: uid = URL(url, 'private' not in request.form, alias).uid session.commit() return redirect(url_for('display', uid=uid)) return render_template('new.html', error=error, url=url) @expose('/display/<uid>') def display(request, uid): url = URL.query.get(uid) if not url: raise NotFound() return render_template('display.html', url=url) @expose('/u/<uid>') def link(request, uid): url = URL.query.get(uid) if not url: raise NotFound() return redirect(url.target, 301) @expose('/list/', defaults={'page': 1}) @expose('/list/<int:page>') def list(request, page): pass
Wieder einmal ziemlich viel Code, aber das meiste ist normale Formularvalidierung. Wir erstellen zwei Funktionen, new und display, und dekorieren sie mit unserem expose-Dekorator aus dem utils-Modul. Dieser Dekorator fügt eine neue Regel zur URL-Map hinzu, indem er alle Parameter zum Konstruktor eines Rule-Objekts übergibt und den Endpoint auf den Namen der Funktion setzt. Damit können wir einfach URLs zu den Funktionen erzeugen – wir nutzen ihre Funktionsnamen als Endpoint.
Denke daran, dass dieser Code nicht unbedingt eine gute Idee für größere Anwendungen ist. In solchen Fällen ist es besser, den vollen Importnamen mit einem allgemeinen Prefix oder etwas Ähnlichem als Endpoint zu nutzen. Sonst wird es ziemlich verwirrend.
Die Formularvalidierung in der new-Methode ist ziemlich simpel. Wir kontrollieren, ob die aktuelle HTTP-Methode POST ist. Falls ja nehmen wir die Daten vom Request und validieren sie. Wenn dort kein Fehler auftritt, erstellen wir ein neues URL-Objekt, übergeben es der Datenbank und leiten auf die Anzeige-Seite um.
Die display-Funktion ist nicht viel komplizierter. Die URL-Rule erwartet einen uid-Parameter, welchen die Funktion entsprechend akzeptiert. Danach holen wir das URL-Objekt mit der angegebenen UID und geben das Template aus, dem wir wiederum das URL-Objekt übergeben.
Wenn die URL nicht existiert, werfen wir eine NotFound-Exception, welche eine statische “404 Seite nicht gefunden”-Seite anzeigt. Wir können diese später mit einer speziellen Fehlerseite ersetzen, indem wir die Exception auffangen, bevor die HTTPException geworfen wird.
Die View-Funktion link wird von unseren Models in der short_url-Eigenschaft genutzt und ist die kurze URL, die wir vermitteln. Wenn also die URL-UID foobar ist, wird die URL unter http://localhost:5000/u/foobar erreichbar sein.
Die View-Funktion list wurde noch nicht geschrieben, das machen wir später. Wichtig ist allerdings, dass die Funktion einen optionalen URL-Parameter akzeptiert. Der erste Dekorator sagt Werkzeug, dass für den Request-Pfad /page/ die erste Seite angezeigt wird (da der Parameter page standardmäßig auf 1 gesetzt wird). Wichtiger ist die Tatsache, dass Werkzeug URLs normalisiert. Wenn du also /page oder /page/1 aufrufst, wirst du in beiden Fällen zu /page/ umgeleitet. Das geschieht automatisch und macht Google glücklich. Wenn du dieses Verhalten nicht magst, kannst du es abschalten.
Und wieder einmal müssen wir zwei Objekte aus dem utils-Modul importieren, die jetzt noch nicht existieren. Eines von diesen soll ein Jinja-Template in ein Response-Objekt verwandeln, das andere prüft eine URL. Fügen wir also diese zu utils.py hinzu:
from os import path from urlparse import urlparse from werkzeug import Response from jinja import Environment, FileSystemLoader ALLOWED_SCHEMES = frozenset(['http', 'https', 'ftp', 'ftps']) TEMPLATE_PATH = path.join(path.dirname(__file__), 'templates') jinja_env = Environment(loader=FileSystemLoader(TEMPLATE_PATH)) jinja_env.globals['url_for'] = url_for def render_template(template, **context): return Response(jinja_env.get_template(template).render(**context), mimetype='text/html') def validate_url(url): return urlparse(url)[0] in ALLOWED_SCHEMES
Im Grunde ist das alles. Die Validierungsfunktion prüft, ob deine URL wie eine HTTP- oder FTP-URL aussieht. Wir machen dies, um uns zu versichern, dass niemand potentiell gefährliches JavaScript oder ähnliche URLs abschickt. Die render_template-Funktion ist auch nicht viel komplizierter, sie schaut nach einem Template im Dateisystem im templates-Ordner und gibt es als Response aus.
Weiterhin fürgen wir die url_for-Funktion in den globalen Kontext des Templates ein, so dass wir auch in Templates URLs erzeugen können.
Da wir jetzt unsere beiden ersten View-Funktionen haben, ist es Zeit, die Templates hinzuzufügen.
Wir haben beschlossen, in diesem Beispiel Jinja-Templates zu nutzen. Wenn du weißt, wie man Django-Templates nutzt, sollte es dir bekannt vorkommen; wenn du bis jetzt mit PHP gearbeitet hast, kannst du Jinja-Templates mit Smarty vergleichen. Wenn du bis jetzt PHP als Templatesprache genutzt hast, solltest du für dein nächstes Projekt mal Mako ansehen.
Sicherheitswarnung: Wir nutzen hier Jinja, welches eine textbasierte Template-Engine ist. Da Jinja nicht weiß, womit es arbeitet, musst du, wenn du HTML-Templates erstellst, alle Werte maskieren, die irgendwann an irgendeinem Punkt irgendeines der folgenden Zeichen enthalten können: >, < oder &. Innerhalb von Attributen musst du außerdem Anführungszeichen maskieren. Du kannst Jinjas |e-Filter für normales Escaping benutzen. Wenn du true als Argument übergibst, maskiert es außerdem Anführungszeichen (|e(true)). Wie du in den Beispielen unterhalb sehen kannst, maskieren wir die URLs nicht. Der Grund dafür ist, dass wir keine & in den URLs haben und deshalb ist es sicher, auf Escaping zu verzichten.
Der Einfachheit halber werden wir HTML 4 in unseren Templates nutzen. Wenn du etwas Erfahrung mit XHTML hast, kannst du sie in XHTML schreiben. Aber beachte, dass das Beispiel-Stylesheet unten nicht mit XHTML funktioniert.
Eine coole Sache, die Jinja von Django übernommen hat, ist Templatevererbung. Das bedeutet, dass wir oft genutzte Stücke in ein Basistemplate auslagern und es mit Platzhaltern füllen können. Beispielsweise landen der Doctype und der HTML-Rahmen in der Datei templates/layout.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <title>Shorty</title> </head> <body> <h1><a href="{{ url_for('new') }}">Shorty</a></h1> <div class="body">{% block body %}{% endblock %}</div> <div class="footer"> <a href="{{ url_for('new') }}">Neu</a> | <a href="{{ url_for('list') }}">Liste</a> | Benutze Shorty für Gutes, nicht für Böses </div> </body> </html>
Von diesem Template können wir in unserer templates/new.html erben:
{% extends 'layout.html' %} {% block body %} <h2>Erstelle eine Shorty-URL!</h2> {% if error %}<div class="error">{{ error }}</div>{% endif -%} <form action="" method="post"> <p>Gebe die URL an, die du kürzen willst</p> <p><input type="text" name="url" id="url" value="{{ url|e(true) }}"></p> <p>Optional kannst du der URL einen merkbaren Namen geben</p> <p><input type="text" id="alias" name="alias">{# #}<input type="submit" id="submit" value="Mach!"></p> <p><input type="checkbox" name="private" id="private"> <label for="private">mache diese URL privat, also zeige sie nicht auf der Liste</label></p> </form> {% endblock %}
Wenn du dich über den Kommentar zwischen den beiden input-Elementen wunderst, das ist ein sauberer Trick, die Templates sauber zu halten ohne Leerzeichen zwischen beide Elemente zu setzen. Wir haben ein Stylesheet vorbereitet, welches dort keine Leerzeichen erwartet.
Ein zweites Template für die Anzeige-Seite (templates/display.html):
{% extends 'layout.html' %} {% block body %} <h2>Verkürzte URL</h2> <p> Die URL {{ url.target|urlize(40, true) }} wurde gekürzt zu {{ url.short_url|urlize }}. </p> {% endblock %}
Jinjas urlize-Filter übersetzt eine URL in einem Text in einen klickbaren Link. Wenn du ihm einen Integer übergibst, wird er den angezeigten Text auf diese Anzahl Zeichen kürzen; wenn du true als zweiten Parameter übergibst, wird ein nofollow-Flag hinzugefügt.
Da wir jetzt unsere ersten beiden Templates fertig haben, ist es Zeit, den Server zu starten und auf den Teil der Anwendung zu schauen, der bereits funktioniert: neue URLs hinzufügen und weitergeleitet werden.
Jetzt ist es Zeit, etwas anderes zu machen: ein Design hinzufügen. Designelemente sind normalerweise in statischen CSS-Stylesheets definiert. Also müssen wir einige statische Dateien irgendwo ablegen – aber das ist ein wenig kompliziert. Wenn du mit bis jetzt mit PHP gearbeitet hast, wirst du gemerkt haben, dass es hier nichts gibt, was eine URL zum Dateisystempfad übersetzt und so direkt auf statische Dateien zugreift. Du musst dem Webserver oder unserem Entwicklungsserver explizit sagen, dass es einen Pfad gibt, der die statischen Dateien beinhaltet.
Django empfiehlt eine separate Subdomain und einen eigenen Server für die statischen Dateien, was eine sehr gute Idee für Umgebungen mit hoher Serverlast ist, aber zu viel des Guten für diese simple Anwendung.
Hier also folgendes Vorgehen: Wir lassen unsere Anwendung die statischen Dateien ausliefern, aber im Produktionsmodus solltest du dem Apachen mitteilen, dass er diese Dateien selbst ausliefern soll. Das geschieht mit Hilfe der Alias-Direktive in der Konfiguration von Apache:
Alias /static /path/to/static/files
Das ist um einiges schneller.
Und wie sagen wir unserer Anwendung, dass sie den Ordner mit statischen Dateien als /static verfügbar machen soll? Glücklicherweise ist das ziemlich einfach, da Werkzeug dafür eine WSGI-Middleware liefert. Es gibt zwei Möglichkeiten, diese zu integrieren: Entweder wrappen wir die ganze Anwendung in diese Middleware (diesen Weg empfehlen wir wirklich nicht) oder wir wrappen nur die Ausführungsfunktion (viel besser, weil wir die Referenz auf das Anwendungsobjekt nicht verlieren). Also gehen wir zurück zur application.py und passen den Code ein wenig an.
Als Erstes musst du einen neuen Import hinzufügen und den Pfad zu den statischen Dateien ermitteln:
from os import path from werkzeug import SharedDataMiddleware STATIC_PATH = path.join(path.dirname(__file__), 'static')
Es wäre besser, die Pfad-Manipulation in die utils.py-Datei zu verschieben, weil wir den Template-Pfad bereits dort ermittelt haben. Aber das ist nicht wirklich von Interesse, und wegen der Einfachheit können wir es im Anwendungsmodul lassen.
Wie können wir also die Ausführungsfunktion wrappen? Theoretisch müssen wir einfach self.__call__ = wrap(self.__call__) schreiben, doch leider klappt das so nicht in Python. Es ist aber nicht viel schwieriger: Benenne einfach __call__ in dispatch um und füge eine neue __call__-Methode hinzu:
def __call__(self, environ, start_response): return self.dispatch(environ, start_response)
Jetzt können wir in unsere __init__-Funktion gehen und die Middleware zuschalten, indem wir die dispatch-Methode einschieben:
self.dispatch = SharedDataMiddleware(self.dispatch, { '/static': STATIC_PATH })
Das war jetzt nicht schwer. Mit diesem Weg können wir WSGI-Middlewares in der Anwendungsklasse einhaken!
Eine andere gute Idee ist es, unserer url_map im utils-Modul den Ort unserer statischen Dateien mitzuteilen, indem wir eine Regel hinzufügen. Auf diesem Weg können wir URLs zu den statischen Dateien in den Templates generieren:
url_map = Map([Rule('/static/<file>', endpoint='static', build_only=True)])
Jetzt können wir unsere templates/layout.html-Datei wieder öffnen und einen Link zum style.css-Stylesheet hinzufügen, welches wir danach erstellen werden:
<link rel="stylesheet" type="text/css" href="{{ url_for('static', file='style.css') }}">
Das geht natürlich in den <head>-Tag, wo zur Zeit nur der Titel festgelegt ist.
Du kannst jetzt ein nettes Layout gestalten oder das Beispiel-Stylesheet nutzen. In beiden Fällen musst du es in die Datei static/style.css einfügen.
Jetzt wollen wir alle öffentlichen URLs auf der “List”-Seite auflisten. Das sollte kein großes Problem sein, aber wir wollen auch eine Art Seitenumbruch haben. Da wir alle URLs auf einmal ausgeben, haben wir früher oder später eine endlose Seite, die Minuten zum Laden benötigt.
Beginnen wir also mit dem Hinzufügen einer Pagination-Klasse zu unserem utils-Modul:
from werkzeug import cached_property class Pagination(object): def __init__(self, query, per_page, page, endpoint): self.query = query self.per_page = per_page self.page = page self.endpoint = endpoint @cached_property def count(self): return self.query.count() @cached_property def entries(self): return self.query.offset((self.page - 1) * self.per_page) \ .limit(self.per_page).all() has_previous = property(lambda x: x.page > 1) has_next = property(lambda x: x.page < x.pages) previous = property(lambda x: url_for(x.endpoint, page=x.page - 1)) next = property(lambda x: url_for(x.endpoint, page=x.page + 1)) pages = property(lambda x: max(0, x.count - 1) // x.per_page + 1)
Dies ist eine sehr einfache Klasse, die das meiste der Seitenumbrüche für uns übernimmt. Wir können ihr eine unausgeführte SQLAlchemy-Abfrage (Query) übergeben, die Anzahl der Elemente pro Seite, die aktuelle Seite und den Endpoint, welcher für die URL-Generation benutzt wird. Der cached_property-Dekorator funktioniert, wie du siehst, fast genau so wie der normale property-Dekorator, mit der Ausnahme, dass er sich das Ergebnis merkt. Wir werden diese Klasse nicht genau besprechen, aber das Grundprinzip ist, dass ein Zugriff auf pagination.entries die Elemente für die aktuelle Seite ausgibt und dass die anderen Eigenschaften Werte zurückgeben, sodass wir sie im Template nutzen können.
Jetzt können wir die Pagination-Klasse in unser Views-Modul importieren und etwas Code zu der list-Funktion hinzufügen:
from shorty.utils import Pagination @expose('/list/', defaults={'page': 1}) @expose('/list/<int:page>') def list(request, page): query = URL.query.filter_by(public=True) pagination = Pagination(query, 30, page, 'list') if pagination.page > 1 and not pagination.entries: raise NotFound() return render_template('list.html', pagination=pagination)
Die If-Bedingung in dieser Funktion versichert, dass ein Statuscode 404 zurückgegeben wird, wenn wir nicht auf der ersten Seite sind und es keine Einträge zum Anzeigen gibt (einen Aufruf von /list/42 ohne Einträge auf dieser Seite nicht mit einem 404 zu quittieren, wäre schlechter Stil).
Und schließlich das Template:
{% extends 'layout.html' %} {% block body %} <h2>URL Liste</h2> <ul> {%- for url in pagination.entries %} <li><a href="{{ url.short_url|e }}">{{ url.uid|e }}</a> » <small>{{ url.target|urlize(38, true) }}</small></li> {%- else %} <li><em>keine URLs bis jetzt gekürzt</em></li> {%- endfor %} </ul> <div class="pagination"> {%- if pagination.has_previous %}<a href="{{ pagination.previous }}">« Vorherige</a> {%- else %}<span class="inactive">« Vorherige</span>{% endif %} | {{ pagination.page }} | {% if pagination.has_next %}<a href="{{ pagination.next }}">Nächste »</a> {%- else %}<span class="inactive">Nächste »</span>{% endif %} </div> {% endblock %}
Jetzt, da wir unsere Anwendung fertig gestellt haben, können wir kleine Verbesserungen vornehmen, zum Beispiel eigene 404-Fehlerseiten. Das ist ziemlich einfach. Das Erste, was wir machen müssen, ist eine neue Funktion namens not_found in den Views zu erstellen, welche ein Template ausgibt:
def not_found(request): return render_template('not_found.html')
Dann müssen wir in unser Anwendungsmodul wechseln und die NotFound-Exception importieren:
from werkzeug.exceptions import NotFound
Schließlich müssen wir sie auffangen und in eine Response umwandeln. Dieser except-Block kommt vor den except-Bock mit HTTPException:
try: # das bleibt das gleiche except NotFound, e: response = views.not_found(request) response.status_code = 404 except HTTPException, e: # das bleibt das gleiche
Jetzt noch templates/not_found.html hinzufügen und du bist fertig:
{% extends 'layout.html' %} {% block body %} <h2>Seite nicht gefunden</h2> <p> Die aufgerufene Seite existiert nicht auf dem Server. Vielleicht willst du eine <a href="{{ url_for('new') }}">neue URL hinzufügen</a>? </p> {% endblock %}
Dieses Tutorial behandelt alles, was du brauchst, um mit Werkzeug, SQLAlchemy und Jinja anzufangen und sollte dir helfen, die beste Lösung für deine Anwendung zu finden. Für einige größere Beispiele, die außerdem einen anderen Aufbau und Ideen zum Ausführen benutzen, solltest du mal einen Blick auf den Beispielordner werfen.
In diesem Sinne: Viel Spaß mit Werkzeug!