================= Werkzeug Tutorial ================= .. admonition:: 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. .. _TinyURL: http://tinyurl.com/ .. _Django: http://www.djangoproject.com/ .. _Jinja: http://jinja.pocoo.org/ .. _SQLAlchemy: http://sqlalchemy.org/ .. _Rails: http://www.rubyonrails.org/ .. _Pylons: http://pylonshq.com/ .. _Beispiel-Ordner: http://dev.pocoo.org/projects/werkzeug/browser/examples .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall .. _setuptools: http://pypi.python.org/pypi/setuptools .. _Online-Demo: http://werkzeug.pocoo.org/e/shorty/ Teil 0: Die Ordnerstruktur ========================== 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. Teil 1: Die WSGI-Anwendung ========================== 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. Teil 2: Die Utilities ===================== 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``: .. sourcecode:: python 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. Unterbrechung: Und nun etwas komplett anderes ============================================= 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: .. sourcecode:: python #!/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. .. _Script-Dokumentation: script.txt Teil 3: Datenbank-Models ======================== Jetzt können wir die Models erstellen. Da die Anwendung ziemlich einfach ist, haben wir nur ein Model und eine Tabelle: .. sourcecode:: python 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 '' % 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: .. sourcecode:: python 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: .. sourcecode:: pycon Interactive Werkzeug Shell >>> from shorty.models import session, URL Jetzt können wir einige URLs zu der Datenbank hinzufügen: .. sourcecode:: pycon >>> urls = [URL('http://example.org/'), URL('http://localhost:5000/')] >>> URL.query.all() [] >>> session.commit() >>> URL.query.all() [, ] 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: .. sourcecode:: pycon >>> URL('http://werkzeug.pocoo.org/', False, 'werkzeug-webpage') >>> session.commit() Dann fragen wir alle ab: .. sourcecode:: pycon >>> URL.query.filter_by(public=False).all() [] >>> URL.query.filter_by(public=True).all() [, ] >>> URL.query.get('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. .. _exzellente Tutorial: http://www.sqlalchemy.org/docs/04/ormtutorial.html Teil 4: Die View-Funktionen =========================== 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: .. sourcecode:: python 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/') def display(request, uid): url = URL.query.get(uid) if not url: raise NotFound() return render_template('display.html', url=url) @expose('/u/') 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/') 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: .. sourcecode:: python 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. Teil 5: Die Templates ===================== 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``: .. sourcecode:: html+jinja Shorty

Shorty

{% block body %}{% endblock %}
Von diesem Template können wir in unserer ``templates/new.html`` erben: .. sourcecode:: html+jinja {% extends 'layout.html' %} {% block body %}

Erstelle eine Shorty-URL!

{% if error %}
{{ error }}
{% endif -%}

Gebe die URL an, die du kürzen willst

Optional kannst du der URL einen merkbaren Namen geben

{# #}

{% 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``): .. sourcecode:: html+jinja {% extends 'layout.html' %} {% block body %}

Verkürzte URL

Die URL {{ url.target|urlize(40, true) }} wurde gekürzt zu {{ url.short_url|urlize }}.

{% 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. Zwischenschritt: Das Design hinzufügen ====================================== 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: .. sourcecode:: 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python url_map = Map([Rule('/static/', 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: .. sourcecode:: html+jinja Das geht natürlich in den ``-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. .. _Mako: http://www.makotemplates.org/ .. _Beispiel-Stylesheet: http://dev.pocoo.org/projects/werkzeug/browser/examples/shorty/static/style.css Teil 6: Öffentliche URLs auflisten ================================== 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: .. sourcecode:: python 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: .. sourcecode:: python from shorty.utils import Pagination @expose('/list/', defaults={'page': 1}) @expose('/list/') 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: .. sourcecode:: html+jinja {% extends 'layout.html' %} {% block body %}

URL Liste

    {%- for url in pagination.entries %}
  • {{ url.uid|e }} » {{ url.target|urlize(38, true) }}
  • {%- else %}
  • keine URLs bis jetzt gekürzt
  • {%- endfor %}
{% endblock %} Bonus: 404-Fehlerseiten gestalten ================================= 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: .. sourcecode:: python def not_found(request): return render_template('not_found.html') Dann müssen wir in unser Anwendungsmodul wechseln und die `NotFound`-Exception importieren: .. sourcecode:: python 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`: .. sourcecode:: python 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: .. sourcecode:: html+jinja {% extends 'layout.html' %} {% block body %}

Seite nicht gefunden

Die aufgerufene Seite existiert nicht auf dem Server. Vielleicht willst du eine neue URL hinzufügen?

{% endblock %} Abschluss ========= 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! .. _Beispielordner: http://dev.pocoo.org/projects/werkzeug/browser/examples