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. In diesem Tutorial werden wir einen kleinen TinyURL-Klon programmieren, der die URLs in einer Datenbank speichert. Die verwendeten Libraries für diese Anwendung sind Jinja für die Templates, SQLAlchemy für die Datenbank und natürlich Werkzeug für WSGI.
Wir haben uns hier für diese Libraries entschieden, weil wir einen Django-ähnlichen Grundaufbau nachstellen wollen, zum Beispiel view-Funktionen und nicht Rails/Pylons-ähnliche Controller-Klassen mit action-Methoden oder Designer-freundliche Templates.
In Werkzeugs Beispiel-Ordner befinden sich einige Anwendungen die andere Konzepte, Template Engines etc. verfolgen. Dort liegt auch der Quellcode dieser Anwendung.
Du kannst easy_install verwenden, um Jinja und SQLAlchemy zu installieren, solltest du diese Libraries noch nicht besitzen:
sudo easy_install Jinja sudo easy_install SQLAlchemy
Diese Befehle funktionieren auch auf einem Windows-Computer (mit Administratorrechten), 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 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 dann 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 templates/ static/
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 und 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 WSGI-Anwendung programmieren müssen. Eine WSGI-Anwendung ist entweder 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 shorty/application.py Datei, in die wir die WSGI Anwendung packen:
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 für Code für den Anfang... Gehen wir mal Schritt für Schritt durch den Quellcode. Zunächst einmal haben wir einige Imports: Aus dem Paket sqlalchemy holen wir uns eine Factory-Funktion, die eine neue Datenbank-Engine für uns erstellt, die einen Connection-Pool besitzt. Die nächsten paar 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, eine 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. Aber wir werden trotzdem schon ein wenig über diese Objekte sprechen. Das Objekt session ist nicht ein PHP-ähnliches 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 Datenbankeintrages in einer Session geben. Das Objekt metadata ist auch von SQLAlchemy und speichert Informationen über die Datenbanktabellen. Es stellt zum Beispiel eine Funktion bereit, um alle definierten Tabellen in der Datenbank zu erstellen.
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. Solltest du PHP kennen, ist die URL-Map ähnlich einem eingebauten mod_rewrite.
Zusätzlich importieren wir hier unser views-Modul, das die view-Funktionen besitzt und dann das models-Modul, dass unsere Models speichert. 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 also 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 gleich mal 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 Datenbank-Tabellen zu erstellen, die wir verwenden.
Und 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 dem environ arbeiten zu müssen, dann binden wir die Anwendung an das local-Objekt für den aktuellen Kontext.
Dann erstellen wir einen URL-Adapter, indem wir die URL-Map an das aktuelle WSGI-Environment 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 bauen oder eben 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 dem Matchen 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 view-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 werkzeug import Local, LocalManager from werkzeug.routing import Map, Rule from sqlalchemy import MetaData from sqlalchemy.orm import create_session, scoped_session 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)
Als erstes 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, aber 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-Skripten. 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 zum Glück unglaublich einfach, solche Verwaltungs-Skripte zu schreiben. Der folgende Code implementiert ein voll funktionsfähiges Verwaltungs-Skript 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 sowieso 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 ein Traceback bekommen, kontrolliere bitte die betreffende Code-Zeile und vergleiche sie mit dem entsprechenden Code in dieser Anleitung.
Sobald das Skript 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 1: 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.
In dem 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 hinzufü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 erstellen:
>>> URL('http://werkzeug.pocoo.org/', False, 'werkzeug-webpage') >>> session.commit()
Und alle abfragen:
>>> 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, da wir einige Daten in der Datenbank haben und wir ungefähr wissen auf welche Weise SQLAlchemy funktioniert, ist es Zeit unsere Views zu erstellen.
Nachdem wir mit SQLAlchemy herumgespielt haben, können wir zurück zu Werkzeug gehen und anfangen, unsere Viewfunktionen zu erstellen. Das Wort “Viewfunktion” kommt von Django, das die Funktionen, die die Templates wiedergibt, “Viewfunktionen” nennt. Deshalb ist unser Beispiel 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.
Zum Anfang erstellen wir einfach eine Viewfunktion für neue URLs und eine Funktion, die eine Nachricht über einen neuen Link darstellt. Das ganze geht in unsere noch leere 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 Formvalidierung. Wir erstellen zwei Funktionen, new und display, und dekorieren sie mit unserem expose-Dekorator von den Utils. Dieser Dekorator fügt eine neue URL-Rule zur Map hinzu, indem er alle Parameter zum Konstruktor eines Ruleobjekts ü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 Formvalidierung in der new-Methode ist ziemlich simpel. Wir kontrollieren, ob die aktuelle 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 akzeptiert. Danach schauen wir in der URL-Rule mit der gegebenen UID und geben das Template aus, indem wir das URL-Objekt ihm ü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 Errorseite ersetzen, indem wir die Exception auffangen, bevor die HTTPException geworfen wird.
Die link-Viewfunktion 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 list-Viewfunktion 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 Requestpfad /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 macht Google glücklich und geschieht automatisch. 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. Eine von diesen soll ein Jinja-Template in ein Response-Objekt ausgeben, das andere prüft eine URL. Fügen wir also diese zu utils.py hinzu:
for 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.
Eine andere Sache, die wir hier machen ist, dass wir die url_for-Funktion in den globalen Templatecontext packen, sodass wir URLs auch in den Templates erstellen können.
Da wir jetzt unsere beiden ersten View-Funktionen haben, ist es Zeit, die Templates hinzuzufügen.
Wir haben beschlossen, Jinja-Templates in diesem Beispiel 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 anschauen.
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 escapen, die irgendwann, an irgendeinem Punkt, irgendeines der folgenden Zeichen enthalten können: >, < oder &. Innerhalb von Attributen musst du außerdem Anführungszeichen escapen. Du kannst Jinjas |e-Filter für normales Escaping benutzen, wenn du true as Argument übergibst escaped es außerdem Anführungszeichen (|e(true)). Wie du in den Beispielen unterhalb sehen kannst, escapen wir die URLs nicht. Der Grund dafür ist, dass wir keine & in den URLs haben und deshalb ist es sicher, Escaping wegzulassen.
Zwecks Einfachheit 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 Beispielstylesheet unten nicht mit XHTML funktioniert.
Eine coole Sache, die Jinja von Django übernommen hat, ist Templatevererbung. Templatevererbung bedeutet, dass wir oft genutzte Stücke in ein Basistemplate legen 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>
Und wir können von diesem Template 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, aber keine Leerzeichen zwischen beide Elemente zu setzen. Wir haben ein Stylesheet vorbereitet, welches dort keine Leerzeichen erwartet.
Und dann 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(s) 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. Also müssen wir einige statische Dateien irgendwo hinlegen – 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 auf statischen Dateien direkt 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, mit Hilfe der Alias-Direktive in der Apachekonfiguration:
Alias /static /path/to/static/files
Das ist um einiges schneller.
Aber wie sagen wir unserer Anwendung, dass sie den Ordner mit statischen Dateien als /static verfügbar machen soll? Glücklicherweise ist das ziemlich einfach, weil Werkzeug dafür eine WSGI-Middleware liefert. Jetzt gibt es zwei Möglichkeiten, diese Middleware zu integrieren: Ein Weg ist, die ganze Anwendung in diese Middleware zu wrappen (diesen Weg empfehlen wir wirklich nicht) und der andere ist, die Ausführungsfunktion zu wrappen (viel besser, weil wir die Referenz zu dem Anwendungsobjekt nicht verlieren). Also gehen wir zurück zu application.py und verändern den Code ein wenig.
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 packen, weil wir den Templatepfad bereits dort ermittelt haben. Aber das interessiert nicht wirklich, 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__) sagen, aber leider klappt das 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” erstellen:
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 zu dem 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 ist.
Du kannst jetzt ein nettes Layout designen oder das Beispielstylesheet nutzen. In beiden Fällen musst du es in die Datei static/style.css packen.
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 eine Pagination-Klasse in unser 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 gehen und die NotFound-Exception importieren:
from werkzeug 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 nicth 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.
Viel Spaß mit Werkzeug!