Django für Python-Neulinge

Diese Themen sollen hier abgedeckt werden:

  1. Python installieren und Django einrichten
  2. Grundlegendes in Sachen Python
  3. Django
  4. Paradigmen und Apps
  5. Der ORM (oder: warum wir kein SQL brauchen) Teil I
  6. Teil II
  7. Sicherheit
  8. Deployment von Django-Sites und -Apps

Vorrausgesetzt werden
  1. Kenntnisse im Umgang mit der Shell
  2. Grundlegende Kenntnisse über Python
  3. Grundkenntnisse in der Webprogrammierung: HTTP, HTML, Server
  4. Eigeninitative

Die Teile werde ich hier nach und nach posten. Oben in der Liste steht der aktuelle Stand, ein Großteil ist schon geschrieben und muss nur noch mal auf Fehler durchgeschaut werden.

Repository auf GitHub mit HTML-Version zum Ausdrucken: https://github.com/enkore/django-tutorial
Lizenz: CC BY-NC-SA 3.0 // Anfragen bezgl. kommerzieller Verwendung an public+kommerz at enkore punkt de .

Wenn euch das Tutorial gefällt, bitte diesen Beitrag liken und oben auf "Bewertung" gehen :)
 
Zuletzt bearbeitet:
Python installieren und Django einrichten

Python installieren und Django einrichten

Bei einigen Betriebssystemen (lies: fast alles, was nicht Windows ist) liegt Python in einer mehr oder weniger aktuellen Version bei. Bei Ubuntu und den meisten anderen Linux-Distributionen ist zumindest Python 2.6 oder 2.7 vorinstalliert, weitere populäre Pakete können bei Bedarf nachinstalliert werden. Das ist insofern ganz praktisch, als dass Django sowieso Python 2.5 oder neuer benötigt. Mit Python 3 funktioniert die aktuelle Version (1.4.1) von Django nicht.
Auf anderen Linux-Distributionen kann Python mit dem Paketmanager Ihres Vertrauens installiert werden, hierbei auch auf die Version achten. Debian hat noch eine etwas ältere Version in den Repos (2.6), die geht aber auch.
Für Windows gibt es fertige Installer auf der offiziellen Python-Website zum herunterladen (Python Programming Language – Official Website), die "Bittigkeit" (32-Bit oder 64-Bit) spielt dabei erstmal keine Rolle.
Mac OS X liefert Python 2.7 mit.

Hier enden die Unterschiede zwischen den Plattformen, denn jetzt haben wir bereits eine funktionierende Python-Umgebung. Da wir jedoch noch Python-Pakete installieren möchten, benötigen wir noch einen kleinen Zusatz. Unter Linux ist dieser meistens schon vorhanden, wenn der Befehl "easy_install" funktioniert, kann der folgende Abschnitt übersprungen werden.

Die meisten Linux-Distributionen haben ein Paket namens python-setuptools, das installiert werden sollte. Sollte das nicht existieren oder kein Linux verwendet werden, gibt es hier detaillierte Instruktionen zum Einrichten.

Nun da wird die sogennanten Setuptools auch in unserer Python-Umgebung verfügbar haben, können wir ganz bequem mit Administratorrechten Pakete mit einem Befehl installieren - Linuxer kennen das.
Wir öffnen also ein Terminal mit Administratorrechten und setzen den Befehl "easy_install django" ab. Wenn alles funktioniert hat, sollte die letzte Ausgabe des Programms etwa "Finished processing dependencies for django" lauten.
Damit wäre Django installiert. Das war einfach, oder?
 
Zuletzt bearbeitet:
Grundlegendes in Sachen Python

Da sich dieses Tutorial an Pythonneulinge richtet (Eigeninitiative ist natürlich gefordert, ich werde hier *nicht* die allergrundlegenste Syntax oder die built-in Funktionen oder Module erklären), möchte ich kurz auf einige Besonderheiten der Sprache Python eingehen.

Der augenfälligste Unterschied beim Betrachten von Python-Quelltexten ist die Methode, die für die Markierung von Blöcken verwendet wird. Anstatt wie in C-ähnlichen Sprachen geschweifte Klammern oder wie in BASIC/Pascal-artigen Sprachen mit Schlüsselwörtern wie if...endif oder begin...end zu arbeiten, wird in Python eingerückt.
Das hat einige Vorteile aber natürlich auch Nachteil. Der größte Vorteil ist die fest in der Sprache verankerte einfache visuelle Identifikation von Codeblöcken.
Der größte Nachteil: Es lässt sich trefflich über das wie der Einrückung streiten. Der Python-Interpreter akzeptiert alles von einem Leerzeichen bis hin zu mehreren Tabs. Vor über einem Jahrzehnt hat man sich jedoch darauf geeinigt vier Leerzeichen zum Einrücken zu verwenden.
Ich für meinen Teil finde es jedoch schwachsinnig mit Leerzeichen einzurücken und nehme deswegen Tabs, weil da jeder selbst entscheiden kann, wie breit die Einrückungen wirklich sind.

Abseits von Einrückungen ist Python immer schon darauf bedachten gewesen eine einfache und mächtige Syntax zu bieten. Daher erkläre ich kurz einige der mächtigsten (und coolsten) Sprachfeatures:

Closures

In Python ist es möglich fast jedes Sprachelement fast überall zu verwenden, daher lassen sich auch Klassen und Funktionen in Funktionen verschachteln. Closures sind deswegen ein häufig verwendetes Paradigma, insbesondere bei Dekoratoren (weiter unten).

Code:
def Closure(closure_argument):
	closure_variable = False
	if closure_argument == "test":
		closure_variable = True
	def Enclosed(enclosed_argument):
		print "%s %s" % (closure_variable, enclosed_argument)
	Enclosed("Closures sind cool")

Closure("test")
Closure("keintest")

Probieren wir das gleich mal aus:
Code:
$ python
Python 2.7.3 (default, Aug  1 2012, 05:14:39) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def Closure(closure_argument):
...     closure_variable = False
...     if closure_argument == "test":
...             closure_variable = True
...     def Enclosed(enclosed_argument):
...             print "%s %s" % (closure_variable, enclosed_argument)
...     Enclosed("Closures sind cool")
... 
>>> Closure("test")
True Closures sind cool
>>> Closure("keintest")
False Closures sind cool

Okay, was haben wir da gemacht? Zuerst haben wir die Funktion Closure(), die eine Variable closure_variable hat. Die ist nur dann True, wenn das Argument von Closure() "test" ist. Dann definieren wir in der Funktion Closure() eine zweite Funktion Enclosed().
Weil aber Enclosed() innerhalb von Closure() definiert wurde, erbt sie auch den Scope (Variablen etc.) von Closure(). Deswegen ist es möglich, dass Enclosed() die Variable closure_variable nutzen kann.
Schließlich rufen wir am Ende von Closure() noch Enclosed() auf. Sehr häufig wird hier die innere Funktion nicht aufgerufen, sondern zurückgegeben. Dazu später mehr. Wichtig dabei ist, dass die Variablen von Closure() in Enclosed() auch dann noch gültig sind, wenn die Funktion Closure() längst verlassen wurde, indem sie bspw. die Funktion Enclosed() zurückgibt.

Mit Closure sind also sehr viele coole Sachen machbar, aber richtig mächtig wird dieses Konstrukt erst mit

Decorators
Decorators oder Dekoratoren sind eigentlich nur ein winzig kleines Stück syntaktischer Zucker, aber dennoch unfassbar praktisch.

Schauen wir uns kurz die Notation an....
Code:
@Decorator
def Decorated(var):
	print var

...und was das eigentlich bewirkt:
Code:
def Decorated(var):
	print var
Decorated = Decorator(Decorated)

Wer dieses kurze Schnipsel verstanden hat, weiß bereits worum es geht: Die Funktion Decorated wird überschrieben und zwar mit dem Rückgabewert der Funktion Decorator.

Ein etwas vollständigeres Beispiel:
Code:
def Decorator(funktion):
	def Wrapper(var):
		if var == True:
			funktion(var)
	return Wrapper

@Decorator
def Decorated(var):
	print var

Decorated(False)
Decorated(True)

Beim Aufruf von Decorated() wird nun nicht mehr Decorated() aufgerufen, sondern die von Decorator() zurückgegebene Funktion Wrapper(). Wrapper() testet das Argument auf Wahrheit und ruft ggf. die ursprüngliche Funktion auf.

Dadurch entsteht dann die folgende Ausgabe:
Code:
>>> Decorated(False)
>>> Decorated(True)
True

Wenn wir nun die Zeile @Decorator mit einer Raute auskommentieren...
Code:
def Decorator(funktion):
	def Wrapper(var):
		if var == True:
			funktion(var)
	return Wrapper

# @Decorator
def Decorated(var):
	print var

Decorated(False)
Decorated(True)
...erhalten wir dann diese Ausgabe:
Code:
>>> Decorated(False)
False
>>> Decorated(True)
True

Generatoren

Generatoren verhalten sich im Grunde ähnlich wie normale Listen bzw. wie alles iterierbare auch.
Hier ist es z.B. irrelevant, ob daten eine Liste, ein Tupel oder ein Generatorobjekt ist:
Code:
for element in daten:
	print element

Das hier würde nur mit normalen Listen und ähnlichem funktionieren:
Code:
for element in daten:
	print element

for element in daten:
	foo(element)

Damit wäre zwar die Frage des wie, aber nicht des was geklärt.

Was sind diese Generatoren nun genau? Wie werden sie erstellt?

Generatoren sind, wie aus den kleinen Beispielen oben ableitbar, wohl etwas ähnliches wie Iterables (Listen, Tupel, Sets, ...), können aber nur ein einziges mal abgerufen werden.
Erstellt werden sie von Generatorfunktionen.
Klassisch würde man eine Liste z.B. so erzeugen:
Code:
def MacheListe():
	liste = []
	for zahl in range(10):
		liste.append(zahl)
	return liste
Liste wird angelegt, befüllt und zurückgegeben.
Nun, wenn wir statt range(10) mal range(4294967296) einsetzen, wird der Nachteil dieser Methode klar: Die Liste wird komplett im Speicher gehalten.

Wenn wir also wissen, dass die Liste nur einmal gebraucht wird, könnten wir hier nun eine Generatorfunktion draus bauen:
Code:
def MacheListe2():
	for zahl in range(10):
		yield zahl
yield ist uns nun unbekannt. Beim Aufruf dieser Funktion kommt ein Generatorobjekt zurück. Das enthält keine Werte. Wenn jetzt auf die Werte des Generatorobjekts zugegriffen werden soll, dann lässt Python die Funktion ablaufen. Und zwar bis ein yield erreicht wird. Dann wird der Wert, der über yield (ähnlich wie return) zurückgegeben wird als aktueller Wert des Generatorobjekts zurückgegeben. Endet die Funktion einfach ohne ein yield zu erreichen, signalisiert Python das Ende des Generatorobjekts (gleiche Exception wie beim Ende einer Liste).
Das ist vielleicht etwas schwer zu fassen, daher ein einfaches Beispiel von oben fortgesetzt:
Code:
def MacheListe2():
	for zahl in range(10):
		yield zahl

for element in MacheListe2():
	print element
Es sollte diese Ausgabe erscheinen:
Code:
... 
0
1
2
3
4
5
6
7
8
9
>>>
Das stand ja zu erwarten, aber es sieht wirklich exakt so aus, als hätten wir MacheListe() aufgerufen...
...deswegen ergänzen wir mal MacheListe2():
Code:
def MacheListe2():
	for zahl in range(10):
		print "MacheListe2: %s" % zahl
		yield zahl
	print "Schiff iste fertig"

for element in MacheListe2():
	print element
Und stellen mit (mehr oder weniger großem) Erstaunen fest,
Code:
... 
MacheListe2: 0
0
MacheListe2: 1
1
MacheListe2: 2
2
MacheListe2: 3
3
MacheListe2: 4
4
MacheListe2: 5
5
MacheListe2: 6
6
MacheListe2: 7
7
MacheListe2: 8
8
MacheListe2: 9
9
Schiff iste fertig
>>>
das MacheListe2() jedes mal ein kleines Stückchen weiter ausgeführt wird und erst dann endgültig verlassen wird, wenn alle Werte erzeugt wurden. Hätten wir den Rückgabewert von MacheListe2() gespeichert, könnten wir jetzt kein zweites Mal auf die Werte zugreifen.
Auch ist es nicht möglich den Index-Operator [] zu benutzen:
Code:
>>> MacheListe2()[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'generator' object has no attribute '__getitem__'

Dafür werden die Werte nie als ganzes im Speicher behalten, was bei der Verarbeitung von großen (oder unendlichen) Datenmengen große Vorteile bietet.

Module

Python bietet, wie jede bessere Skriptsprache :)P) auch, ein ausgereiftes Modulsystem.
Das alles zu erklären, wäre etwas viel, daher nur die Grundzüge in Kürze:

Ein Modul wird mittels des import-Statements importiert. Das hat verschiedene Variationen:
Code:
import os # Simpel
import re, urllib2, sys # Mehrere Module auf einmal gehen auch
from django import models # Importiert das Modul models aus dem Modul django
import os.path as pathtools # Importiert das Modul os.path unter dem Namen pathtools
from django import forms as f # Imporiert forms von django als f

Der Vorteil des Modulsystems ist, dass man sich genau aussuchen kann, welche Symbole (Klassen, Funktionen usw.) man benutzen möchte. Vergleiche man dies einmal mit z.B. C oder PHP, wo es nur die Option "Alles oder gar nichts" gibt, ist das sehr komfortabel. Außerdem können Module nicht den globalen Namensraum verschmutzen.
Dadurch werden weniger Namen belegt, außerdem ist immer klar, zu welchem Modul eine Klasse oder Funktion gehört.

Module selbst erstellen

Ein Modul erstellen ist sehr einfach. Ein Modul ist nämlich nur eine Python-Quelltextdatei in einem Verzeichnis, in dem eine Datei namens "__init__.py" liegt.

-- package
+--- __init__.py
+--- modul.py

"package" ist dabei sozusagen das Übermodul (es gibt Packages nicht wirklich, das sind auch nur Module) von modul. Wenn man ein "import package" in diesem Fall machen würde, wird in Wahrheit ein "import package.__init__ as package" gemacht. Oft steht in der __init__ aber auch einfach nichts ;)

Das soll es erstmal an blanken Python-Kenntnissen sein.

Weiterführende Erklärungen

http://stackoverflow.com/a/231855/675646
http://stackoverflow.com/a/1594484/675646
http://wiki.python.org/moin/PythonDecorators
 
Zuletzt bearbeitet:
Django

Django

Django (www.djangoproject.com) ist ein Webframework, dass vollständig in Python geschrieben ist und auf ein MVC-Modell aufsetzt. Django wurde mit dem Ziel geschrieben einfach und schnell Webanwendungen jeder Art zu erstellen und folgt dem Python-Motto "batteries included" durch die Mitlieferung zahlreicher nützlicher Zusatzmodule und -tools.
So bietet Django von Haus aus einen sehr mächtigen ORM (Object-Relational-Mapper, also eine Zugriffsschicht für relationale SQL-Datenbanken), ein eigenes Templatesystem, Nutzerauthentifizierung, Internationalisierung, einen Inhaltseditor, Funktionen zur Erzeugung und Validierung von Formularen, Logging, Sitzungsverwaltungsfunktionen, ein Kommantersystem, Cachingmechanismen und diverse Sicherheitsmodule wie ein Clickjacking und CSRF-Schutz.
Klingt gut? Klingt gut! Außerdem ist Django natürlich Open-Source und steht unter der äußerst liberalen BSD-Lizenz, sodass weder eure Anwendnugen noch eventuelle Patches veröffentlicht werden müssen.

Architektur

Eine Django-Site besteht aus einem Ordner, der alles weitere enthält. So gibt es immer ein Skript namens manage.py, welches für alle administrativen Tätigkeiten (Server starten und stoppen, Datenbank updaten, Anwendungen erzeugen, ...) genutzt wird.
Dann gibt es ein Modul mit allen Einstellungen (settings.py), das von Django automatisch eingelesen und allen Anwendungen zur Verfügung gestellt wird. In diesem Modul steht unter anderem, welche Datenbank genutzt werden soll und welche Anwendungen aktiv sind.
Außerdem gibt es eine root-URLconf, ein Modul in welchem eine Zuordnung von URLs zu Apps hergestellt wird.
Im Ordner der Django-Site können dann noch beliebig viele Anwendungen liegen.

Eine Anwendung ist wieder ein Ordner. Eine Anwendung hat meistens ein views-Modul, aber meistens gibt es auch noch ein models-Modul und diverse andere.
Eine Anwendung (App) könnte z.B. eine Blog-App sein, oder ein Forums-App. Nahezu 100 % der Funktionalität einer Django-Site liegen in den jeweiligen Anwendungen.
Obwohl Apps erstmal voneinander getrennt sind, ist es natürlich auch möglich diese zu verbinden und z.B. gemeinsame Daten, Templates usw. zu nutzen.

Da Django ein MVC (Model View Controller) Framework ist, enthalten Apps meist alle drei Elemente. Dabei ist jedoch die Terminologie etwas anders:
  • Models nach MVC sind auch bei Django Models. Ein Model speichert Daten in der Datenbank.
  • Controller nach MVC heißen bei Django Views und implementieren die eigentliche business logic.
  • Views nach MVC werden bei Django Templates genannt. Ein Template wird von einer View aufgerufen und stellt Daten dar. Templates werden meistens zur Erzeugung von HTML genutzt, es können aber beliebige textbasierte Formate erzeugt werden.

Eine Anfrage wird also von einer View beantwortet. Eine View ist eine einfache Funktion nach dem Schema view(request[, argumente]). Der erste Parameter ist ein request-Objekt und enthält alle Daten zur Anfrage. Der Rückgabewert ist eine HTTPResponse-Instanz und wird zum Browser zurückgeschickt.
Was dazwischen passiert, ist Sache der View. Es kann alles gemacht werden :)
So können Daten mittels des ORM aus der Datenbank geholt werden und hübsche HTML-Seiten mittels Templates erzeugt werden. Templates sind einfache Textdateien mit einer simplen aber turingvollständigen Sprache. Alternativ bietet Django auch die Möglichkeit andere Templatesysteme einzubinden.

Damit Django weiß, welche View wofür zuständig ist, verfügt jede App über eine eigene URLconf. Das ist ein Modul, das eine Liste von regulären Ausdrücken und den zuzuordnenden Viewfunktionen enthält. Dabei können auch sehr komplexe Muster und Ausdrücke realisiert werden, weil Django die URLconfs nur einmal einließt und dann in kompilierter Form im RAM vorhält.

Erstellen einer Django-Site

...ist einfach und schnell erledigt. Wir öffnen ein Terminal und geben folgendes ein:
Code:
$ django-admin.py startproject djangotut
(Je nach Distribution heißt das Tool nicht django-admin.py, sondern einfach nur django-admin. Wenn Django mittels easy_install installiert wurde, ist es immer django-admin.py.)

Ein Ordner namens djangotut wird erstellt, der die oben angesprochene manage.py (unsere Django-Kontaktperson) und einen weiteren Ordner namens djangotut enthält. In letzteren befinden sich drei Dateien (plus die __init__.py, damit es ein Package ist):
  • urls.py ist die Haupt-URLconf
  • settings.py enthält alle Einstellungen
  • wsgi.py enthält die WSGI-App

Exkurs: WSGI?

WSGI ist eine standardisierte Schnittstelle im Python-Bereich um Webanwendungen an Webserver anzubinden. Es gibt Module für FastCGI, Apache (mod_wsgi) und viele andere Möglichkeiten. In der Regel nutzt man nur die ersten beiden.
Django selbst enthält auch einen eigenen Webserver, der aber nur für Entwicklungszwecke gedacht ist.

Einstellungssache

Die settings.py müssen wir jetzt etwas bearbeiten, u.a. müssen wir eine Datenbank einstellen. Der Einfachheit halber nehmen wir SQLite, was im Lieferumfang von Python (...batteries included...) bereits enthalten ist.

Code:
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'djangotut/db.sqlite3',
    }
}

Wer mag, kann noch einige andere Details anpassen:

Code:
TIME_ZONE = 'Europe/Berlin'
LANGUAGE_CODE = 'de-de'

Der Rest passt erstmal so.


Daten zur Bank bringen

Jetzt müssen wir erstmal Django die Datenbank initialisieren lassen. Dazu rufen wir das manage.py-Skript mit dem Befehl syncdb auf:
Code:
$ ./manage.py syncdb
Creating tables ...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_user_permissions
Creating table auth_user_groups
Creating table auth_user
Creating table django_content_type
Creating table django_session
Creating table django_site

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'mabe'): mabe
E-mail address: ....
Password: 
Password (again): 
Superuser created successfully.
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

Damit wären wir soweit einen ersten Blick auf unser bisheriges Werk zu werfen...

Entwicklungsserver starten

Das ist, wie so vieles bei Django, eine einfache Prozedur. Wieder hilft uns hier manage.py:
Code:
$ ./manage.py runserver
Validating models...

0 errors found
Django version 1.4, using settings 'djangotut.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Wenn wir nun auf die angegebene URL gehen, beglückt uns dieser freudige Text:
It worked!
Congratulations on your first Django-powered page.

Das sieht doch schonmal gar nicht so übel aus.
 
Zuletzt bearbeitet:
Paradigmen und Apps

Paradigmen und Apps

Im vorigen Teil wurden schon einige Paradigmen angesprochen, insbesondere das MVC-Prinzip von Django. Das möchte ich jetzt vertiefen und mich dabei auf Apps konzentrieren.

Administration: App erstellen

Für dieses Kapitel benötigen wir eine einfache App. Dazu erstellen wir die App "blog" mit unserem V-Mann manage.py:
Code:
$ ./manage.py startapp blog
Keine Ausgabe ist Erfolg ;)

Das erstellt im Grunde nur den Ordner blog und kopiert ein paar Standarddateien dorthin.

Models

Models werden von fast allen Apps benötigt, weil sie die Methode zum Speichern von Daten darstellen. Ein Model ist grundsätzlich immer an eine App gebunden und zwar an jene, in welcher es definiert wird.
Ein Model wird durch eine Klasse in dem Modul models einer App definiert. Die Attribute dieser Klasse sind die einzelnen Felder und, auf einem niedrigeren Level, meistens auch äquivalent zu den einzelnen Spalten der Tabelle. Die meisten Models werden intern als eine einzige Tabelle dargestellt und jeder Datensatz ist eine Instanz des Models.
Allerdings gibt es auch Felder, welche eine eigene Tabelle einführen, so z.B. Many-To-Many-Felder, die eine Beziehung zwischen mehreren Instanzen eines Models herstellen.
Ein einfaches Beispiel zur Illustration:

Code:
from django.db import models
from django.contrib.auth.models import User

class Entry(models.Model):
	title = models.CharField(max_length=200)

	time = models.DateTimeField(auto_now=True)

	author = models.ForeignKey(User)

	content = models.TextField()

	def __unicode__(self):
		return self.title

Zuerst holen wir uns das models-Modul aus dem Django-ORM, der die Basisklasse für Datenmodelle liefert und in dem auch die Felder definiert sind, die Django im Kern mitliefert. Eine Übersicht aller Felder findet sich hier.
Dazu gesellt sich das User-Model des Authentifizierungsmoduls von Django.

Danach definieren wir das Modell Entry, was so z.B. als sehr einfaches Model für einen Blog herhalten könnte. Das Feld title ist ein einfaches Textfeld, welches in einer MySQL-Datenbank als VARCHAR abgelegt werden würde. time ist ein Feld für Datum- und Zeitwerte, was aufgrund der Option auto_now automatisch beim Speichern mit der aktuellen Zeit belegt wird.
Das Feld author ist ein normaler Fremdschlüssel auf das User-Model, also eine Referenz auf einen Nutzer.
Schließlich gibt es noch das Feld content als Textfeld.

Weiterhin definieren wir noch die Methode __unicode__ die von Django immer dann verwendet wird, wenn das Objekt als solches ausgegeben wird. Hier geben wir einfach den Titel zurück.

Django erzeugt auch noch einige Sachen automatisch, so z.B. eine Unterklasse namens Meta, die man auch selber definieren kann, und das Attribut pk, was ein Primary Key für jede Instanz ist. Definiert man einen eigenen Primary Key ist pk ein Alias für den ersten definierten Primary Key

Zugriff auf Instanzen

Die Modelklasse hat standardmäßig ein Attribut objects, was der sogenannte Manager ist. Der Manager ist die Schnittstelle von der Klasse zu den Instanzen jener, die ja noch in der Datenbank liegen. Es gibt da sehr vielfältige Möglichkeiten und da der ORM von Django sehr mächtig ist, wird nur sehr selten reines SQL benötigt.
Durch die Verwendung des ORMs wird übrigens auch eine extrem häufig auftretende Sicherheitslücke vollständig eleminiert: Die gefürchtete SQL-Injection.
Erst wenn man händisches SQL verwendet, schafft man wieder Möglichkeiten dafür.

Der Manager liefert, außer bei wenigen Methoden wie z.B. get(), immer QuerySets zurück. Diese evaluieren zu Listen, wenn man drüberiteriert oder auf Elemente zugreift, ansonsten... bleiben sie einfach ein QuerySet, das sich weiter manipulieren lässt. Der Manager und QuerySets haben weitestgehend die gleichen Methoden. Am häufigsten wird filter() genutzt, eine Funktion, die, nun ja, filtert.
Den Filter spezifiziert man mittels Schlüsselwortargumenten, die aus Feldnamen und optionalen genaueren Spezifikationen zusammengesetzt werden. Davon gibt es eine ganze Menge.
Entry.objects.filter(id=10) würde das Objekt mit der ID 10 holen, Entry.objects.filter(id__in=[1,2,3,4,5]) würde die Objekte mit den IDs 1 bis 5 holen.

Das soll fürs erste reichen, mehr in die Tiefe gehts im ORM-Kapitel. ;)

Views und URLs

Eine View ist der Dreh- und Angelpunkt jeder Seite. Sie ist der eigentliche datenverarbeitende Teil.
In der Regel bekommt jede Seite einer Site eine eigene View, in einem Blog gäbe es so z.B. je eine View für Posts, Kategorien, Übersichten und Archivseiten.
Nun zum Aufbau und der "Bekanntmachung" einer View. Views werden meistens im views-Modul einer App abgelegt. Der grundlegende Aufbau ist immer gleich, eine simple Funktion. So könnte z.B. eine View eines Blogs aussehen:
Code:
from models import Entry

def show_post(request, post_id):
	post = Entry.objects.get(pk=post_id)

	return rufe_template_auf(template="post.html", daten=post)

Der erste Parameter ist immer vorhanden und ist das request-Objekt, das alle Informationen zur HTTP-Anfrage enthält. Meistens interessiert sich die View jedoch höchstens für die POST oder GET-Attribute, die als Dictionary die entsprechenden Werte beherbegen.
Die danach kommenden Parameter entnimmt Django der URL, allerdings benötigt Django dazu natürlich noch Zusatzinformationen. Diese werden in der URLconf hinterlegt.

Die URLconf ist im Grunde eine Zuordnug von regulären Ausdrücken zu Views und wird per Konvention in dem Modul urls abgelegt. Für obige View würde eine URLconf inetwa so aussehen:

Code:
from django.conf.urls import patterns, include, url

urlpatterns = patterns("blog.views",
	url(r"post/(\d+)/$", "show_post"),
)

Django interessiert sich bei dem Modul nur für die Variable urlpatterns, die genau diese Zuordnung enthält und mit der Funktion patterns() angelegt wird.
Der erste Parameter ist dabei das Basismodul der Views für diesen Aufruf von patterns(). In diesem Beispiel wäre die Basis also das Modul views im Package blog (die hypothetische Blog-App), ergo blog.views.
Die restlichen Parameter werden immer mit der Funktion url() angelegt. url() nimmt bis zu fünf Parameter entgegen von denen zwei Pflicht sind:
  • Ein regulärer Ausdruck. Benannte Gruppen werden als Schlüsselwortargumente (kwargs) an die View übergeben, unbennante nach der Reihenfolge im Regex. Mehr zu regulären Ausdrücken in Python
  • Die View. Kann entweder ein String sein, der eine Funktion in dem Basismodul benennt bzw. ein gepunkteter Pfad ist, oder einfach eine Funktion als solche. Hier gehen auch lambda-Ausdrücke.
  • Ein Dictionary mit zusätzlichen Argumenten für die View, nützlich wenn eine View mehrere URLs bedient.
  • Ein Name für diese URL, damit kann sie einfacher in Templates und bei Weiterleitungen referenziert werden.
  • Ein Prefix. Wird praktisch nie genutzt.
Durch die regulären Ausdrücke kann man bereits die Parameter validieren, bei der URLconf oben z.B. wird show_post niemals mit was anderem als einer Zahl aufgerufen (allerdings als String, denn alle Parameter aus den regulären Ausdrücken werden als String übergeben).
Der Regex wird übrigens nur auf den Pfad angewandt, also alles von der Domain bis zum Beginn der GET-Daten (?...). Daher können über die Regexe keine GET-Parameter eingesammelt werden.
Das Dollarzeichen am Ende des Regex ist nicht ganz unwichtig, weil das bedeutet, dass der Regex bis zum Ende matchen muss. Würde man es weglassen und hätte man zwei URLs wie z.B. archive/ und archive/(\d+), so würde immer nur die erste aufgerufen werden, wenn sie zuerst in der URLconf kommt.

Templates

Templates werden in einer einfachen Sprache geschrieben, die auch von Nicht-Programmierern weitestgehend problemlos geschrieben werden können.
Es gibt nur zwei Arten von Statements:
Variablen: {{ variablenname }}
Befehle (Tags): {% befehl parameter .... %}

Variablen können, ähnlich wie Unix-Pipes, durch verschiedene Filter gejagt werden: {{ variable|truncatewords:150 }} würde den Inhalt von variable auf 150 Wörter begrenzen.

Tags können einfache Befehle sein wie z.B. {% include "anderes_template.html" %}, aber auch komplexere if-Statements.

Appspezifische Templates werden üblicherweise als Dateien mit der Endung .html im Unterordner templates der App abgelegt.

Benutzung von Templates

Die Variablen, die ein Template zur Verfügung stehen, werden an zwei Stellen definiert. Erstens als einfaches Dictionary, das an die Templateengine weitergegeben wird, und zweitens über den Context. Zu ~100 % möchte man hier den sogennannten RequestContext (django.template.RequestContext) benutzen, der einen Haufen nützlicher Variablen zur Verfügung stellt und für einige fortgeschrittene Sachen unerlässlich ist.

Damit erklärt sich auch schon fast, wie Templates nun benutzt werden, jetzt muss ich nur noch einen Funktionsnamen fallen lassen: render_to_response(). Wird für ~99 % aller Situationen gebraucht.

django.shortcuts.render_to_response nimmt bis zu vier Parameter entgegen, den Dateinamen des Templates, das schon angesprochene Dictionary, den Kontext und einen MIME-Typen. Bis auf den Dateinamen ist alles optional.

Als einfaches Beispiel muss wieder unsere Post-View von weiter oben herhalten:

Code:
from models import Entry
from django.shortcuts import render_to_response
from django.template import RequestContext

def show_post(request, post_id):
	post = Entry.objects.get(pk=post_id)

	return render_to_response("post.html", {"post": post}, RequestContext(request))
Das dazugehörige Template post.html könnte so aussehen:

Code:
{% load markup %}

<h1>{{ post.title }}</h1>
<em>Authored by {{ post.author.username }} at {{ post.time }}</em>

<div>{{ post.content|markdown:"safe" }}</div>

Wir sollten jetzt eine funktionierende App haben, davon haben wir aber noch nichts - sie ist nicht aktiv!

Administration: Apps

Zum Aktivieren von Apps muss das settings-Modul bearbeitet werden. Konkret geht es um das Tupel INSTALLED_APPS, welches wir nun um unsere App ergänzen:
Code:
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.markup', # <--- für {% load markup %}
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',

    "blog",
)

Anschließend führen wir ein Datenbankupdate durch, um die Tabelle(n) für unsere App zu erstellen:
Code:
$ ./manage.py syncdb
Creating tables ...
Creating table blog_entry
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)
Wie wir sehen wurde die Tabelle blog_entry erstellt, die also zur App blog zum Model Entry gehört (Tabellennamen sind bei Django lowercase).

Außerdem müssen wir noch die URLconf der blog-App in die root-URLconf unter djangotut.urls einbinden. Das geht mit einer Zeile in Parametern von patterns():

Code:
urlpatterns = patterns('',
    url(r'^blog', include('blog.urls')),
)
Damit landen alle URLs von unserer blog-App unter domain/blog/... .

Jetzt könnten wir schon den Server starten und Posts anschauen, aber...

Der Django-Admin

...es gibt ja noch gar keine. Also benötigen wir eine Maske zum Erstellen von Posts. Praktischerweise hat Django sowas eingebaut.

Zuerst aktivieren wir eine Django-App, die mitgeliefert wird:
Code:
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.markup',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin', # <---- auskommentieren
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',

    "blog",
)

...führen wieder ein Datenbankupdate durch...
Code:
$ ./manage.py syncdb
Creating tables ...
Creating table django_admin_log
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

...und müssen noch zwei Kleinigkeiten erledigen. Zum einen die URLs in der root-URLconf:
Code:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^blog', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
)

zum anderen erstellen wir das Modul admin in unserer App, um dem Django-Admin zu verraten, dass unser Model zur Bearbeitung freigeben ist.
Code:
from django.contrib import admin

from blog.models import Entry

admin.site.register(Entry)
Da gibt es noch mehr Möglichkeiten, die wir noch später erlernen werden...

Jetzt können wir den Server starten. Den Django-Admin finden wir unter /admin. Bei dem ersten Datenbankupdate habt ihr einen Superuser erstellt mit dem ihr euch jetzt anmelden könnt. Unter dem Kasten Blog findet ihr den Punkt Entry, rechts "Hinzufügen". Damit könnt Ihr einen neuen Post erstellen und nach getaner Arbeit auf /blog/post/1/ gehen und das Werk dieses Kapitels bestaunen :)

Weiterführende Erklärungen

http://docs.python.org/library/re.html
http://docs.djangoproject.com/en/1.4/ref/models/querysets/
http://docs.djangoproject.com/en/1.4/ref/templates/builtins/
 
Zuletzt bearbeitet:
Der ORM (Teil I)

Der ORM (Teil I)
Nachdem wir uns im vorigen Kapitel bereits die Grundzüge des ORMs angeschaut haben, wird es jetzt Zeit etwas mehr in die Thematik einzusteigen. Gleichzeitig werden wir noch einige Sachen über Views lernen, denn schließlich wird der ORM fast ausschließlich dort verwendet.

Beziehungen zwischen Models
Daten speichern zu können ist zwar eine feine Sache, aber ohne Beziehungen zwischen den Daten herstellen zu können, ist das doch langweilig. Das wissen auch die Django-Entwickler und geben uns gleich drei Möglichkeiten dafür:

m:n-Beziehungen (ManyToManyField)
In einem Blog werden Posts üblicherweise in Kategorien eingeordnet. Dabei kann ein Post oftmals mehreren Kategorien zugeordnet werden (Kategorien könnte man hier auch Tags nennen).
Wenn wir diese Beziehung nun in Code gießen möchten, benötigen wir zwei Models:

  1. Ein Model für Posts, welches wir bereits erstellt haben
  2. Ein Model für Kategorien, welche von Posts referenziert werden
In diesem Fall ist die Beziehung von Kategorien zu Posts eine m:n-Beziehung, es können also beliebig viele Kategorien beliebig viele Posts referenzieren, daher ist das passende Feld für diesen Anwendungsfall das ManyToManyField. Intern wird eine m2m-Tabelle erstellt, die lediglich drei Spalten hat, nämlich den Primary Key, sowie zwei Fremdschlüssel auf die beiden "Enden" der Beziehung, hier also ein Fremdschlüssel für Posts und einer für Kategorien.

Wir müssen das Feld allerdings nur in eines der beiden Models einfügen. Zuerst aber erstellen wir erstmal ein simples Model für Kategorien:

Code:
class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()

    def __unicode__(self):
        return self.name
Jetzt benötigen wir noch das ManyToManyField. Da gibt es aber ein kleines Problem, obwohl wir zwar das Feld in dem Model Category ablegen könnten, würde es erheblich logischer anmuten, wenn die Posts auf die Kategorien verweisen würden. Deswegen müssen wir uns jetzt etwas überlegen um das Model Entry zu verändern. Da fallen mir drei Möglichkeiten ein:

  1. Die gesamte Datenbank löschen, das Model verändern und die Datenbank neu erzeugen (klingt blöd, ist blöd)
  2. Die Datenbankshell öffnen (manage.py dbshell) und mittels DROP TABLE die Tabellen des Models löschen und syncdb ausführen (klingt nach Pfusch, ist Pfusch)
  3. Ein System für Datenbankmigrationen verwenden. Klingt gut, ist gut.
Das liefert Django (noch) nicht mit und muss daher nachinstalliert werden. Das PyPI-Paket nennt sich south (easy_install south). Nach der Installation müssen wir south noch aktivieren, wozu wir den Eintrag "south" im settings-Modul ergänzen:

Code:
INSTALLED_APPS = (
    ....

    "blog",
    "south",
)
Danach muss unbedingt syncdb ausgeführt werden, sonst wird south nicht funktionieren!

Jetzt müssen wir south für unsere Blog-App aktivieren, was folgendermaßen funktioniert:

Code:
$ ./manage.py schemamigration blog --initial
Creating migrations directory at 'djangotut/blog/migrations'...
Creating __init__.py in 'djangotut/blog/migrations'...
 + Added model blog.Category
 + Added model blog.Entry
Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate blog
schemamigration schaut sich unsere Models an und prüft, ob sie seit der letzten Migration verändert wurden. Ist das der Fall wird eine neue Migration erzeugt, die, wenn sie angewendet wird, Datenbank und Modeldefinitionen wieder in Einklang bringt.
Da unser Model bereits in der Datenbank vorhanden ist, müssen wir die Migration gar nicht wirklich anwenden. Sozusagen faken:

Code:
$ ./manage.py migrate blog --fake
Running migrations for blog:
 - Migrating forwards to 0001_initial.
 > blog:0001_initial
   (faked)
Jetzt können wir guten Gewissens unser Model verändern, indem wir das ManyToManyField hinzufügen. Der Parameter blank=True bewirkt, dass dieses Feld nicht mit Werten befüllt werden muss, d.h., dass nicht jeder Entry eine Beziehung zu einer Category hat.

Code:
class Entry(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    time = models.DateTimeField(auto_now=True)
    author = models.ForeignKey(User)

    categories = models.ManyToManyField(Category, blank=True)

    def __unicode__(self):
        return self.title
Anschließend lassen wir die Datenbank einfach von south migrieren:

Code:
$ ./manage.py schemamigration blog --auto
 + Added M2M table for posts on blog.Entry
Created 0002_auto.py. You can now apply this migration with: ./manage.py migrate blog
$ ./manage.py migrate blog
Running migrations for blog:
 - Migrating forwards to 0002_auto.
 > blog:0002_auto
 - Loading initial data for blog.
Installed 0 object(s) from 0 fixture(s)
Und schon haben wir nicht nur die Möglichkeit geschaffen Beziehungen zwischen zwei Models zu definieren, sondern haben auch gleich noch gelernt, wie Datenbankschemata angeglichen werden können! :) Das ist zum Entwickeln extrem praktisch.

Jetzt sollten wir noch das Category-Model beim Django-Admin anmelden. Dann kann man Kategorien anlegen und Posts diesen zuordnen. Zugegeben, dass ist noch kein Wordpress, aber immerhin ;)

1:n-Beziehungen (ForeignKey / Fremdschlüssel)
Sehr häufig ist auch der Fall, dass wir einen Datensatz auf einen anderen verweisen lassen möchten. Das haben wir sogar schon im Entry-Model gemacht und zwar beim Feld author, das ein Fremdschlüssel auf den Benutzeraccount des Autors ist.
Das Konzept ist einfach und extrem weit verbreitet, so weit, dass einige Datenbanken direkte Unterstützung dafür bieten (MySQL mit InnoDB, zum Beispiel).
Trotzdem möchte ich nochmal anhand von Kommentaren demonstrieren, wie das genau funktioniert. Kommentare sind einem Post zugeordnet und genau für sowas gibts Fremdschlüssel. Daher ohne weiteres ado:

Code:
class Comment(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    time = models.DateTimeField(auto_now=True)
    post = models.ForeignKey(Entry)
post verweist auf den Entry, auf den sich der Kommentar bezieht. Das Model sollte auch im Admin registriert werden. Außerdem müssen wir noch die Datenbank aktualisieren, was ab jetzt für unsere App nicht mehr syncdb macht, sondern die Kombination aus schemamigration und migrate. Weiterhin solltet ihr noch eine passende __unicode__()-Methode definieren.
Wir haben außerdem anhand des author-Feldes schon gelernt, wie auf den verwiesenen Datensatz in Templates (und auch in Python) zugegriffen wird, der Wert des Feldes ist schlicht das Objekt.

1:1-Beziehungen (OneToOneField)
Wenn unser Blog ganz groß und erfolgreich ist und viele gute Kommentare geschrieben werden, dann möchten wir vielleicht besonders exzellente Kommentare hervorheben. Sozusagen ein Editor's Pick für Kommentare.
Dies wäre ein Anwendungsfall für eine 1:1-Beziehung: Jeder Post hat maximal einen "Top"-Kommentar und jeder Kommentar gehört sowieso zu maximal einem Post.

Daher ergänzen wir unser Entry-Model mal wieder:

Code:
class Entry(models.Model):
    ...
    featured_comment = models.OneToOneField("Comment", blank=True, null=True)
    ....
Hier verweisen wir auf das Comment-Model mit einem String, anstatt der Klasse. Denn wenn ihr das Comment-Model von oben verwendet habt, müsst ihr es im Modul nach der Definition von Entry platziert haben -- sonst würde es nicht funktionieren. Deswegen ist die Klasse Comment noch nicht vorhanden, wenn wir das OneToOneField definieren. Daher der Verweis per String.
Weiterhin übergeben wir noch blank=True und null=True, weil es sonst nicht möglich wäre einen Post zu erstellen ohne einen Top-Kommentar auszuwählen, allerdings ist es andersrum auch nicht möglich einen Kommentar zu erstellen ohne einen Post erstellt zu haben. Entsprechend möchten wir auch Posts erstellen ohne einen Top-Kommentar anzugeben ;)
Nicht vergessen: Datenbank auf den aktuellen Stand bringen.

In der sonstigen Handhabung verhalten sich OneToOneField exakt wie ForeignKeys und auch von der Funktion her sind sie fast gleich.

Fleißarbeit
Nun da wir etwas an unseren Models gemacht haben, wird es Zeit entsprechend Views und Templates zu ergänzen. Zuerst möchten wir natürlich die Kommentare zu einem Post ausgeben, dazu müssen wir lediglich die Kommentare passend filtern und aus der Datenbank holen und an das Template übergeben:

Code:
def show_post(request, post_id):
    post = Entry.objects.get(pk=post_id)
    comments = Comment.objects.filter(post=post)

    return render_to_response("post.html", {
        "post": post,
        "comments": comments,
    }, RequestContext(request))
Unser Template sollte damit natürlich auch was anfangen, also fügen wir zuerst mal eine Überschrift an, wie man das so kennt; "X Kommentare hierzu":

Code:
<h3>{{ comments|length }} Comments</h3>
Jetzt müssen wir jeden Kommentar ausgeben. Dazu bräuchten wir so eine Art for-Schleife und merkwürdigerweise gibt es auch genau dieses Konstrukt, was die Sache dann doch vereinfacht:

Code:
{% for comment in comments %}
<article class="comment">
    <h4>{{ comment.title }}</h4>
    <em>Posted at {{ comment.time }}</em>

    <div>{{ comment.content|markdown:"safe" }}</div>
</article>
{% endfor %}
Hier benutzen wir wieder den markdown-Filter mit dem Parameter safe, damit einfach formatiert werden kann, aber kein HTML eingeschleust wird. Erwähnte ich bereits, dass das ganze Tutorial hier in Markdown geschrieben ist? :D

So damit werden normale Kommentare angezeigt. Aber noch gar nicht unser Top-Kommentar! Da wir im Template schon das post-Objekt haben, brauchen wir dafür nichts an der View zu ändern, sondern können einfach unser Template ergänzen.
Allerdings wäre es ziemlich blöd, wenn da groß prangt "EDITORS PICK" und dann ein leerer Kasten kommt, weil noch gar kein Kommentar ausgewählt wurde. Also bräuchten wir mit fingern schnippt eine Art if-Konstrukt. Hm, woran diese Django-Entwickler alles gedacht haben, müssen schlaue Kerle sein! :)

Code:
{% if post.featured_comment %}
<h3>Featured Comment</h3>

<article class="comment">
    <h4>{{ post.featured_comment.title }}</h4>
    <em>Posted at {{ post.featured_comment.time }}</em>

    <div>{{ post.featured_comment.content|markdown:"safe" }}</div>
</article>
{% endif %}
Mit dem if schauen wir, ob ein Kommentar festgelegt wurde, wenn ja, zeigen wir ihn an. Wenn ich mir das Template so als ganzes anschaue, sehe ich da aber was ganz unfassbar böses... und zwar Redundanz. Warum sollten wir zweimal den Code für die Ausgabe eines Kommentars vorhalten, denn dann müssen wir das gleich zweimal ändern, wenn wir an der Ausgabe was ändern wollen.
Also wäre es doch praktisch diesen Teil auszulagern. Und auch daran haben die Django-Entwickler gedacht und uns das include-Statement gegeben. Also flugs eine neue Datei angelegt und nur den <article>-Teil hereinkopiert. Ich habe sie "show_comment.html" genannt. Dann können wir im post-Template die Redundanz auf ein Minimum reduzieren (Die Stelle für die regulären Kommentare kriegt ihr schon selbst hin):

Code:
{% if post.featured_comment %}
<h3>Featured Comment</h3>

{% include "show_comment.html" with comment=post.featured_comment only %}
{% endif %}
include ist recht flexibel, so kann das einzufügende Template entweder direkt als String (wie hier) angegeben werden oder mit einer Variable. Zweiter Fall kann z.B. für Rekursion genutzt werden. Dann kann man noch den Kontext des Templates bestimmen, denn include ist nicht wie include in, sagen wir, PHP, wo PHP einfach Copy'n'Paste macht. include ist mehr wie import in Python.
Hier sagen wir mit with comment=..., welchen Wert die Variable comment für das Template haben soll. Anschließend hängen wir noch ein only an, damit nur die comment-Variable im Template existiert und sonst nichts.

Wenn wir jetzt freudig auf unsere Seite gehen, erwartet uns...

TemplateSyntaxError at /blog/post/1/
Invalid filter: 'markdown'
Hm?!
Nun, für Tagbibliotheken wie markup gilt das gleiche wie für Variablen, weil das Template bei include quasi unabhängig vom Haupttemplate gerendert wird. Deswegen müssen wir auch in show_comment.html ein {% load markup %} voranstellen.

Das klingt zwar erstmal unnötig kompliziert, hat aber den riesigen Vorteil, dass Templates unabhängig vom Ort ihrer Einbindung sind. Jedes Template ist für sich vollständig und jedes Template kann unabhängig von einem anderen gerendert werden (abgesehen natürlich von benötigen Templates aufgrund von includes, aber das ist ja klar ;).

So nachdem wir das erledigt haben, dürften wir exakt die gleiche Ausgabe wie vorher erhalten. Nur ist sie sauberer implementiert!

Wer jetzt denkt, dass es das schon war, der hat sich geirrt: scrollt mal ein bisschen nach oben, class Category unso... dafür müssen wir auch noch ein paar Views und Templates entwerfen.

Also erstmal eine View für eine einzelne Kategorie. Was brauchen wir? Die Kategorie. Und vielleicht noch die zugehörigen Posts. Was sitzen wir noch hier rum!? Los Marsch, an die Arbeit!

Code:
def show_category(request, category_id):
    category = Category.objects.get(pk=category_id)
    posts = Entry.objects.filter(categories__in=[category])

    return render_to_response("category.html", {
        "category": category,
        "posts": posts,
    }, RequestContext(request))
Wir holen uns die Objekte (beachtet den field lookup bei categories, das in prüft, ob mindestens einer der Werte von categories im übergebenen Array ist) und dann kommen erstmal vier Zeilen fürs Template. Die sind aber fast genau die gleichen, wie schon bei show_post? Böse? Böse!

Also überlegen wir mal, wie wir das elegant lösen. Dazu erstmal ein paar Rahmenüberlegungen:

  1. Die meisten Views geben Kram mit einem Template aus
  2. Die meisten Views nutzen dabei immer das gleiche Template
  3. Der Hauptunterschied zwischen den Views ist der Name des Templates und das Dictionary mit den Variablen
Hier bietet sich ein Decorator an. Wir nehmen einen Decorator, sagen ihm welches Template wir nehmen wollen und in der View geben wir einfach nur noch ein Dictionary zurück.
Jetzt müssen wir aber mal etwas komplexer werden, weil der Decorator ja einen Parameter entgegen nimmt. Wir erinnern uns

Code:
@Decorator
def X():
    pass
ist genau das gleiche wie

Code:
def X():
    pass
X = Decorator(X)
Wenn wir jetzt einen Parameter übergeben wollen, sähe das so aus:

Code:
@Decorator(param)
def X():
    pass
entspricht

Code:
def X():
    pass
X = Decorator(param)(X)
Das heißt für uns: eine Art doppelte Closure wird gebraucht. Fangen wir an:

Code:
def render_to(template):
    def decorator(function):
        def wrapper(...):
            pass

        return wrapper
    return decorator
render_to() gibt den eigentlichen Decorator zurück, der die zu behängende Funktion als Parameter bekommt. Deswegen muss unser Wrapper, wie bisher auch, im Decorator definiert werden. Doppeltes Closure, sag ich ja! ;)
Jetzt müssen wir uns noch einen Wrapper überlegen, der mit allen Views funktioniert. Views haben, wie wir uns erinnern, immer einen request-Parameter und dann u.U. Positions- und Schlüsselwortargumente.

Code:
def wrapper(request, *args, **kwargs):
    dictionary = function(request, *args, **kwargs)

    return render_to_response(template, dictionary, RequestContext(request))
Mit args und *kwargs fangen wir diese Argumente ein und können sie direkt an eine Funktion weitergeben. Wir rufen also die eigentliche View auf, die ja der decorator() als Parameter bekommen hat, und verwenden den Rückgabewert für render_to_response() in Verbindung mit dem Template-Namen, den wir noch von render_to() geerbt haben.

Code:
def render_to(template):
    def decorator(function):
        def wrapper(request, *args, **kwargs):
            dictionary = function(request, *args, **kwargs)

            return render_to_response(template, dictionary, RequestContext(request))

        return wrapper
    return decorator
Im Prinzip wären wir fertig. Das funktioniert so. Außer wenn die View mal kein dict zurückgibt, dann wird das nicht funktionieren. Wenn man beispielsweise Weiterleitungen macht, dann kommt eine HTTPResponse von der View zurück. Und die kann render_to_response gar nicht verarbeiten. Wir ergänzen also ein kurzes Schnipsel:

Code:
def render_to(template):
    def decorator(function):
        def wrapper(request, *args, **kwargs):
            dictionary = function(request, *args, **kwargs)

            if not isinstance(dictionary, dict):
                return dictionary

            return render_to_response(template, dictionary, RequestContext(request))

        return wrapper
    return decorator
Damit kann die View auch direkt eine HTTPResponse zurückgeben. Voilá, sehr flexibel das ganze. Fürst erste kopieren wir render_to() einfach an den Anfang unseres views-Moduls.
Jetzt können wir die bisherigen Views vereinfachen:

Code:
@render_to("post.html")
def show_post(request, post_id):
    post = Entry.objects.get(pk=post_id)
    comments = Comment.objects.filter(post=post)

    return {
        "post": post,
        "comments": comments,
    }
Seite aufrufen, alles so wie vorher? Perfekt! Wo waren wir eigentlich? Genau show_category(), da mussten wir noch ein Template basteln:

Code:
{% load markup %}

<h1>{{ category.name }}</h1>

<div>{{ category.descriptions|markdown:"safe" }}</div>

{% for post in posts %}
<h1>{{ post.title }}</h1>
<em>Authored by {{ post.author.username }} at {{ post.time }}</em>

<div>{{ post.content|truncatewords:100|markdown:"safe" }}</div>

{{ post.comment_set.all|length }} Comments
{% endfor %}
Beachtet einmal diese Zeile für sich:

Code:
{{ post.comment_set.all|length }} Comments
Wir haben doch gar kein Feld comment_set im Entry-Model definiert!? Ja, schon, aber Django hat das für uns erledigt, weil wir nämlich den ForeignKey von Comment aus angelegt haben. Damit können wir ganz einfach, ohne das explizit in der View tun zu müssen, auf "verwandte" Objekte zugreifen.
Hier zeigt sich auch, warum die Templatesprache von Django toll für Designer ist: Sie ist nicht nur einfach, man muss auch praktisch keine Ahnung haben was man da tut. Hinter comment_set verbirgt sich nämlich ein related manager, der auch für ManyToMany-Felder benutzt wird. Er kann alles, was der normale Manager (z.B. Entry.objects) auch kann und sogar ein bisschen mehr, hier benutzen wir einfach die Methode all() um alle Kommentare zu bekommen. Schließlich lassen wir einfach die Länge ausgeben.
Wir sehen hier auch, dass Django, wenn möglich, versucht Funktionen oder Objekte aufzurufen.
Wenn wir kein Designer sind und ein effizientes System haben wollen, können wir die Zeile noch etwas tunen, indem wir die Datenbank und nicht Djangos' Templatesystem zählen lassen:

Code:
{{ post.comment_set.all.count }} Comments
Aber wozu eigentlich da noch all() aurufen? Jeder Manager verfügt schließlich über die count()-Methode:

Code:
{{ post.comment_set.count }} Comments
Das ließt sich auch gleich viel besser Des Posts Kommentarset ihm seine Anzahl ;)

Wie auch immer; mit diesem Wissen gewappnet solltet ihr schon in der Lage sein, sowohl unsere beiden Views als auch die zugehörigen Templates zu vereinfachen (Wie, keine Lust? Oben stand doch Eigeninitiative ;).

Um sich unsere Kategorien anschauen zu können, fehlt nur noch eine Sache, nämlich ein Eintrag in der URLconf:

Code:
...
url(r"category/(\d+)/$", "show_category"),
...
Wenn ihr zwischenzeitlich mal eine Kategorie angelegt habt und mindestens ein Post da drin ist, dann solltet ihr unser bisheriges Werk auf /blog/category/1/ betrachten können.

Komplexere Queries
Jeder kennt sicherlich diese Archivfunktion von Wordpress, wo man für jedes Jahr und für jeden Monat eine Seite hat mit den Einträgen von diesem Jahr. Also möchten wir erstmal für jedes Jahr und für jeden Monat eine Seite haben.
Natürlich sind wir wieder auf die intelligente Art Faul, sodass wir hier das erste mal für zwei URLs die gleiche View verwenden und ich auch zum ersten Mal das Pferd von hinten aufzäume.
Wir fangen jetzt nämlich mit der URLconf an.
Zuerst der einfache Teil, nämlich das Monatsarchiv:

Code:
url(r"archive/(?P<year>\d{4})/(?P<month>\d{1,2})/$", "show_archive"),
Hier benutze ich die erweiterte Regex-Syntax für benannte Gruppen (year und month), sodass year und month als Schlüsselwortargumente übergeben werden. Das ist hier nur für den Überblick ;)
Wenn wir nämlich die gleiche View für Jahres- und Monatsarchive verwenden wollen, so müssen wir den Parameter month auffüllen, was wir mit dem kwargs-Parameter von url() machen. Klingt kompliziert, ist aber einfach:

Code:
url(r"archive/(?P<year>\d{4})/$", "show_archive", {"month": None}),
Wenn auf /archive/2012/ zugegriffen wird, wird also show_archive(year="2012", month=None) aufgerufen, wenn wir jedoch auf /archive/2012/8 zugreifen, dann wird show_archive(year="2012", month="8") aufgerufen. So können wir innerhalb der View ggf. verschiedene Codepfade einschlagen.

Nun zur View:

Code:
@render_to("archive.html")
def show_archive(request, year, month):
    posts = Entry.objects.filter(time__year=year)

    return {
        "posts": posts
    }
Eigentlich selbsterklärend, der Field Lookup year filtert anhand eines Date- oder DateTime-Felds die Einträge, je nach dem, ob das gespeicherte Datum ins Jahr fällt.
Interessant wird es eigentlich erst, wenn wir uns jetzt überlegen, wie wir die Monate handhaben wollen. posts ist jetzt ein QuerySet, das in jedem Fall nur noch für die Einträge des Jahres steht, unabhängig davon, ob month gültig ist, oder nicht.
Wenn ich euch jetzt verrate, dass filter() akkumulativ arbeitet, solltet ihr die Lösung schon vor Augen haben:

Code:
@render_to("archive.html")
def show_archive(request, year, month):
    posts = Entry.objects.filter(time__year=year)
    if month:
        posts = posts.filter(time__month=month)

    return {
        "posts": posts
    }
Wenn month gültig ist, wird zusätzlich nach dem Monat gefiltert. Der Field Lookup month funktioniert intuitiverweise genauso wie year, nur eben für Monate. Wenn ihr jetzt einen Eintrag mit verstellter Systemzeit erstellt (z.B. im nächsten Monat), könnte ihr schön sehen, wie die Filter arbeiten, wenn ihr auf die Archivseiten geht.

Um jetzt aber mal einen wirklich etwas komplexeren Query zu basteln, wollen wir die Anzahl der Posts für jedes Jahr in unsere Archivseite integrieren.

Ha, jetzt hab ich euch eiskalt erwischt, was? Kein Wunder, so einfach ist das auch nicht!

Was wollen wir eigentlich. Das ist bei komplexen Angelegenheiten immer gut zu wissen.
Wir wollen:

  1. Eine Liste von Jahren ohne Doppeleinträge
  2. Die Anzahl der Zeilen/Posts für jedes Jahr
  3. Am besten noch sortiert
Django selbst kann fast alles, aber eben doch nicht alles. Hierfür nutzen wir das erste mal extra(), eine besonders mächtige Funktion, die es erlaubt SQL-Bedingungen bzw. Zusätze zu definieren. Extra hat einen Haufen Schlüsselwortargumente; wir nutzen hier erstmal nur das select-Argument, mit welchem wir zusätzliche Felder erzeugen können, indem wir ein Dictionary übergeben von dem die Schlüssel auf Namen und die Inhalte auf den Selektor gepackt werden.
Damit werden wir das Jahr aus dem time-Feld der Entries extrahieren. Das ist DBMS-abhängig, da wir SQLite nutzen, geht das am einfachsten mit der SQL-Funktion strftime():

Code:
years = Entry.objects.extra(select={
    "year": "strftime('%%Y', time)"
})
stftime() formatiert hier die time-Spalte (von unserem time-Feld, manchmal sind interna doch wichtig, deswegen werden sie ja dokumentiert) auf eine vierstellige Jahreszahl. Damit wir da aber dran kommen und das QuerySet nicht automatisch Objekte draus bastelt, wird nur die gerade erzeugte ("virtuelle") year-Spalte übernommen. Dafür gibt es eine extra Methode namens values(), die eine Liste von Spaltennamen entgegennimmt:

Code:
years = Entry.objects.extra(select={
    "year": "strftime('%%Y', time)"
}).values("year")
years ist jetzt eine Liste von Dictionaries mit dem Schlüssel years und den zugehörigen Ausgaben von strftime von jeder Zeile. Das können wir schonmal mittels der order_by()-Methode ordnen, dabei wollen wir das neueste Datum zuerst haben und sortieren deswegen absteigend (-Feldname):

Code:
years = Entry.objects.extra(select={
    "year": "strftime('%%Y', time)"
}).values("year").order_by("-year")
Wir haben jetzt Ziele 1 und 3 erreicht. Jetzt noch die Anzahl. Dazu gibt es ein weiteres cooles Features: Annotations. Damit kann man diverse Aggregationsfunktionen auf verwandte Objekte anwenden, z.B. Count() zum Zählen.
Diese Funktionen gehören zum django.db.models-Modul und muss daher importiert werden:

Code:
from django.db.models import Count

...

years = Entry.objects.extra(select={
    "year": "strftime('%%Y', time)"
}).values("year").order_by("-year").annotate(Count("id"))
Damit werden automagisch die Anzahl der Ergebnisse für die id-Spalte zu jeder zugehörigen Ergebniszeile hinzugefügt. Klingt kompliziert und ehrlich gesagt ist es das auch ;)

Das Ergebnis von diesem Query sieht dann z.B. so aus:

Code:
[{'id__count': 1, 'year': u'2013'}, {'id__count': 4, 'year': u'2012'}]
Jetzt basteln wir noch ein Template zu der Sache (vergesst nicht, dass years noch ins Rückgabe-Dictionary muss):

Code:
{% load markup %}

<ul>
{% for year in years %}
<li>{{ year.year }} ({{ year.id__count }} posts)</li>
{% endfor %}
</ul>

{% for post in posts %}
<h1>{{ post.title }}</h1>
<em>Authored by {{ post.author.username }} at {{ post.time }}</em>

<div>{{ post.content|truncatewords:100|markdown:"safe" }}</div>

{{ post.comment_set.count }} Comments
{% endfor %}
Links auf Objektinstanzen
Ist euch eigentlich schonmal aufgefallen, dass wir bisher noch keinen einzigen Link gesetzt haben? Was soll denn so eine Archivansicht mit gekürzten Posts oder eine Kategorieansicht mit selbigen, wenn man gar nicht zum eigentlichen Post kommt!?

Natürlich bietet Django hier gleich mehrere zielführende Strategien und eine, die genau für diesen Anwendungsfall, das verlinken von Objektinstanzen (z.B. einzelne Posts oder Kategorien) gedacht ist: get_absolute_url()

get_absolute_url() ist eine Modelmethode, die einen absoluten Pfad zurückgibt, da aber das hardcoden von Pfaden unschön ist, haben sich die Django-Entwickler auch dafür eine elegante Lösung ausgedacht:

Code:
class Model(models.Model):
    ...

    @models.permalink
    def get_absolute_url(self):
        return ("pfad zur view",
            [positionsargumente],
            {schlüsselwortargumente})
Das implementieren wir dann mal direkt für Entry und Category:

Code:
class Category(models.Model):
    ...

    @models.permalink
    def get_absolute_url(self):
        return ("blog.views.show_category", [self.pk])

class Entry(models.Model):
    ...

    @models.permalink
    def get_absolute_url(self):
        return ("blog.views.show_post", [self.pk])
Der Dekorator erzeugt dann mittels der reverse()-Funktion die entsprechende URL.

Benutzt wird das in Templates z.B. so:

Code:
...
{% for post in posts %}
<h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
<em>Authored by {{ post.author.username }} at {{ post.time }}</em>
....
Fortsetzung folgt
 
Zuletzt bearbeitet:
Teil II
Pagination
Ein überaus wichtiges Feature, welches wir bisher komplett vernachlässigt haben, ist Pagination. Wenn unser Blog 4242 Posts in einer Kategorie hat, würde die Seite ziemlich lang werden. Deswegen ist eine Aufteilung in mehrere Seiten eine gar nicht so blöde Idee.

Praktischerweise hat Django ein paar Klassen mit an Board, um soetwas effektiv zu handhaben. Allerdings müssen wir unsere Views ergänzen:

Code:
from django.core.paginator import Paginator
...

@render_to("category.html")
def show_category(request, category_id):
    ...
    posts = Paginator(category.entry_set.all(), 15).page(int(request.GET.get("page", 1)))

    return {
        ...
        "posts": posts,
    }
Paginator ist eine Klasse, die einen Satz Objekte und die Objekte pro Seite entegennimmt. Die page()-Methode nimmt einen Integer größer null entgegen und gibt ein entsprechendes Seitenobjekt zurück, hier wird die Seite über den Parameter page bestimmt, wird dieser nicht übergeben, nehmen wir die erste Seite. Das Seitenobjekt enthält dann u.a. die Objekte der Seite und Informationen über vorhergehende und noch kommende Seiten.

Im Template sieht meine Umsetzung dann so aus:

Code:
{% for post in posts.object_list %}
...
{% endfor %}

{% if posts.has_previous %}
<a href="{{ category.get_absolute_url }}?page={{ posts.previous_page_number }}">Previous page</a>
{% endif %}

{% if posts.has_next %}
<a href="{{ category.get_absolute_url }}?page={{ posts.next_page_number }}">Next page</a>
{% endif %}
Pagination wäre auch sehr sinnvoll für die Archivseiten, weil die noch viel schneller voll werden als Kategorien.

Formulare
Wenn wir uns unsere Post-Seite anschauen, werden dort zwar Kommentare zur Schau gestellt, aber neue Kommentare können nur über den Django-Admin angelegt werden. Das ist ein überaus unwünschenswerter Zustand. Optimal wäre ein einfaches Formular unter jedem Post mit dem jeder einen Kommentar verfassen kann.

Und jetzt alle im Chor: Natürlich haben die Django-Entwickler daran gedacht

Und das schöne: Forms sind genauso einfach zu handhaben wie Models. Also legen wir gleich los!

Code:
from django import forms

...

@render_to("post.html")
def show_post(request, post_id):
    class CommentForm(forms.Form):
        title = forms.CharField(max_length=200)
        content = forms.CharField()

    ...
Ich sagte ja: wie Models. Ein Formular wird durch eine Klasse repräsentiert deren Attribute einzelne Felder definieren.
Manchmal gibt es jedoch verschiedene Arten bestimmte Werte abzufragen, so kann man Text mit einem einfachen Textfeld (<input type="text"... >), wie es z.B. für den Titel passend wäre, oder mit einer Textarea. Dafür gibt es verschiedene Widgets. Das Widget wird bei der Felddefinition einfach mit angegeben:

Code:
class CommentForm(forms.Form):
    title = forms.CharField(max_length=200)
    content = forms.CharField(widget=forms.Textarea)
Damit wird bei content das Standardwidget TextInput durch Textarea ersetzt.

Jetzt haben wir zwar eine wunderschöne Formularklasse, aber noch keinerlei Funktionalität. Dazu müssen wir noch zwei, drei Zeilen schreiben:

Code:
...
if request.method == "POST":
    form = CommentForm(request.POST)
    if form.is_valid():
        pass
        # Formulardaten verarbeiten
else:
    form = CommentForm()

return {
    ...
    "form": form,
}
Damit wird, sofern kein Formular abgesandt wurde (Anfragemethode GET), ein leeres Formular erzeugt und ans Template übergeben, während beim Absenden des Formulars (POST) die Formularklasse die Daten erhält.
Die Methode is_valid() prüft anhand von einigen Kriterien, ob das Formular gültig ist. Hier würde z.B. geschaut werden, ob title länger als 200 Zeichen ist und wenn dem so ist eine Exception erzeugt werden.
Wenn is_valid() True zurückgibt, so sind die Daten im Formular gültig und können weiter verwendet werden. Wir möchten hier einfach nur einen Kommentar erstellen, daher schreiben wir:

Code:
if request.method == "POST":
    form = CommentForm(request.POST)
    if form.is_valid():
        comment = Comment(
            title=form.cleaned_data["title"],
            content=form.cleaned_data["content"],
            post=post)
        comment.save()
else:
    ...
Jetzt ist hier nur noch ein kleiner Schönheitsfehler drin: Wenn der Nutzer das Formular abschickt und dann die Seite aktualisiert, wird sein Kommentar nochmal gepostet. Daher möchten wir auch hier das übliche Schema für Formulare verwenden, nämlich eine Weiterleitung bei erfolgreicher Verarbeitung des Formulars:

Code:
if request.method == "POST":
    ...
    if form.is_valid():
        ...
        return redirect(post)
redirect() ist eine sehr nützliche Funktion, weil sie entweder eine Model-Instanz entgegennimmt (dann wird get_absolute_url() genutzt um die Ziel-URL herauszufinden), oder den Namen einer View oder auch einfach eine URL.
Jetzt zahlt sich auch aus, dass unser render_to() Dekorator alles, was kein Dict ist, einfach durchlässt, weil der Rückgabewert von redirect() eine Instanz von HttpResponseRedirect ist.

Um das Formular jetzt testen zu können, müssen wir natürlich noch die Ausgabe im Template hinzufügen:

Code:
<form action="." method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
Für einfachere Anforderungen kann ein Formular sich selbst rendern, später werden wir noch lernen, wie wir die Ausgabe eines Formulars ganz fein beeinflussen können. Ansonsten ist hier nicht viel spektakuläres dabei, höchstens noch das {% csrf_token %}, welches ein Teil von Djangos standardmäßig aktiviertem <a href="http://de.wikipedia.org/wiki/Cross-Site_Request_Forgery">CSRF</a>-Schutz ist.

Probierts mal aus und lasst den Tag weg: Ihr könnt das Formular nicht mehr abschicken, weil der CSRF-Schutz die Bearbeitung der Anfrage abbricht, noch bevor sie überhaupt zu unserer View gelangt.

Eine Sache stört mich aber noch: Django kennt unser Model doch schon mit allen Details, da könnte Django doch eigentlich selber ein passendes Formular erstellen! "könnte"? Kann!

<h4><a href="https://docs.djangoproject.com/en/dev/topics/forms/modelforms/">Formulare für Models</a></h4>
Dafür gibt es eine extra Klasse: ModelForm. Damit wird unser Code etwas vereinfacht, aber vor allem müssen wir nicht mehr an zwei Stellen arbeiten, wenn wir Comment zusätzliche Felder geben (was wir später auch noch tun werden):

Code:
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
Damit würde ein Formular mit drei Feldern erstellt werden:

  1. Titel
  2. Inhalt
  3. Post als Drop-Down mit allen Posts
Das Dritte möchten wir aber eigentlich gar nicht. Kein Problem:

Code:
class Meta:
    ...
    exclude = ["post"]
Jetzt haben wir nur noch die beiden Felder, die wie auch haben wollen. Jetzt müssen wir noch etwas Code ändern:

Code:
if request.method == "POST":
    ...
    if form.is_valid():
        comment = form.save(commit=False)
        comment.post = post
        comment.save()
        ....
else:
    form = CommentForm()
Normalerweise, also wenn das Formular vollständig wäre mit dem post-Feld, dann könnten wir das mit commit=False sein lassen, denn dann könnte das Formular das Model so abspeichern, wie es ist. So lassen wir uns aber von der save() ein noch nicht gespeichertes Comment-Objekt geben, wo wir noch das Feld post setzen und es dann erst speichern.

Vorher würde das Speichern fehlschlagen, weil Django weiß, dass post gesetzt sein muss.

So jetzt sind wir mit unserem Kommentarformular fertig. Nein eigentlich nicht. Denn obwohl der Kommentar sofort sichtbar ist, wäre es doch nett, den Nutzer noch explizit darauf hinzuweisen, dass sein Kommentar erfolgreich abgeschickt wurde.
Halt, Stopp, was tust du da? Wolltest du gerade eine zusätzliche Variable comment_posted ans Template geben und einen "Juhu, Sie habens geschafft!"-Text ausgeben, wenn sie True ist? Nein, so machen wir das hier nicht. Überlegt mal, was für ein riesiger Aufwand es wäre das so bei vielen Nachrichten zu machen...

...Django bietet da eine viel komfortablere, template-unabhängige Möglichkeit:

<h3><a href="https://docs.djangoproject.com/en/1.4/ref/contrib/messages/">Messages</a></h3>
Django hat ein eingebautes System um Statusnachrichten verschiedener Klassen dem Nutzer anzuzeigen. Damit reduziert sich der Aufwand auf exakt eine Zeile in der View. Zuerst business as usual; Modul importieren:

Code:
from django.contrib import messages
Dann sich überlegen, wo wir die Nachricht brauchen, wir machen da weiter, wo wir aufgehört haben, beim Kommentarformular:

Code:
if request.method == "POST":
    ...
    if form.is_valid():
        ...
        # Hier Nachricht anweisen
        return redirect(post)
Django bringt fünf Klassen für Nachrichten standardmäßig mit und definiert für sie gleich noch Kurzfunktionen in der Form funktion(request, Nachricht)

  1. debug messages.debug()
  2. info messages.info()
  3. success messages.success()
  4. warning messages.warning()
  5. error messages.error()
Der Fall oben sollte eindeutig sein:

Code:
if request.method == "POST":
    ...
    if form.is_valid():
        ...
        messages.success(request, "Comment posted.")
        return redirect(post)
Jetzt noch ein bisschen generischer Template-Code:

Code:
{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}
Und ausprobieren.

Wenn ich mich recht erinnere, hatte ich weiter oben allerdings etwas von "template-unabhängige Möglichkeit" geschrieben. Damit das messages-System aber auf allen Seiten funktioniert müssten wir folgerichtig das Schnipsel von weiter oben in jedes Template kopieren, was aber offensichtlich keine gute Idee ist. Jetzt kommen

Templates mit Vererbung
ins Spiel. Denn wenn wir eine Seite erstellen, gibt ist das drumherum immer gleich - das gleiche html-Element mit dem fast gleichen head-Element, wo dann der Titel von Seite zu Seite anders ist, wahrscheinlich immer das gleiche Menü, die gleiche Seitenleiste - ihr merkt schon.

Zuerst basteln wir uns also dieses drumherum:

Code:
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Titel - Blog</title>
        <style type="text/css">
        </style>
    </head>
    <body>

        <div class="navbar">
            <div class="navbar-inner">
                <div class="container">
                    <a class="brand" href="/blog">Blog</a>

                    <ul class="nav">
                        <li><a href="/blog">Blog</a>
                    </ul>
                </div>
            </div>
        </div>

        <div class="container">
            <div class="row">
                <div class="span2">
                    Sidebar
                </div>

                <div class="span8" id="content">
                    <h1 id="page-title">Titel</h1>

                    {% if messages %}
                    <ul class="messages">
                        {% for message in messages %}
                        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
                        {% endfor %}
                    </ul>
                    {% endif %}

                    Inhalt
                </div>
            </div>
        </div>
    </body>
</html>
Zum Einfüllen der Platzhalter bringt Django natürlich auch etwas mit und zwar Blöcke. Ein Block sieht so aus:

Code:
{% block name %}Inhalt des Blocks{% endblock %}
Dann machen wir das mal...

Code:
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>{% block pagetitle %}Seitentitel nicht gesetzt{% endblock %} - Blog</title>
        <style type="text/css">
        </style>        
    </head>
    <body>

        <div class="navbar">
            <div class="navbar-inner">
                <div class="container">
                    <a class="brand" href="/blog">Blog</a>

                    <ul class="nav">
                        <li><a href="/blog">Blog</a>
                    </ul>
                </div>
            </div>
        </div>

        <div class="container">
            <div class="row">
                <div class="span2">
                    Sidebar
                </div>

                <div class="span8" id="content">
                    <h1 id="page-title">{% block title %}Titel nicht gesetzt{% endblock %}</h1>

                    {% if messages %}
                    <ul class="messages">
                        {% for message in messages %}
                        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
                        {% endfor %}
                    </ul>
                    {% endif %}

                    {% block content %}Inhalt nicht gesetzt{% endblock %}
                </div>
            </div>
        </div>
    </body>
</html>
Dieses Template erstellen wir unter dem Namen base.html in einen extra Ordner templates in djangotut, weil wir es für die gesamte Site nutzen möchten. Gleich daneben können wir dann noch einen Ordner templates_static für später erstellen. Den Ordner müssen wir noch in der settings registrieren:

Code:
TEMPLATE_DIRS = (
    "djangotut/templates",
    ...
)
Die Warnung, dass man nur absolute Pfade verwenden soll, kann man erstmal getrost ignorieren. In Produktivumgebungen sollten es aber aus Sicherheitsgründen auf jeden Fall absolute Pfade sein!

Nun können wir dieses Haupttemplate in unsere Templates (hier erstmal post.html) einbinden, dies geschieht mittels des extends-Tags, der auf jeden Fall der erste Templatetag in einem Template sein muss:

Code:
{% extends "base.html" %}
Wenn wir jetzt auf eine Postseite gehen, sollten wir den Inhalt von base.html sehen, weil wir die Platzhalter noch nicht aufgefüllt haben, was wir jetzt tun wollen:

Code:
{% extends "base.html" %}
{% load markup %}

{% block title %}{% block pagetitle %}
    {{ post.title }}
{% endblock %}{% endblock %}
Hier verschachtele ich die beiden Blöcke title und pagetitle, weil sie in diesem Fall den gleichen Wert haben sollen. Jetzt weiter im Text:

Code:
{% block content %}

<em>Authored by {{ post.author.username }} at {{ post.time }}</em>

<div>{{ post.content|markdown:"safe" }}</div>

<hr>

{% if post.featured_comment %}
<blockquote>
    <h3>Featured Comment</h3>

    {% include "show_comment.html" with comment=post.featured_comment only %}
</blockquote>
{% endif %}

<h3>{{ post.comment_set.count }} Comments</h3>

{% for comment in post.comment_set.all %}
{% include "show_comment.html" with comment=comment only %}
{% endfor %}

<form action="." method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>

{% endblock %}
Und schon sollten die Platzhalter gefüllt werden. Das solltet ihr auch mit den anderen Templates so machen, danach werden wir uns anschauen, wie man etwas mehr Metadaten in die Sache bringt.

Metadaten
Schauen wir uns nochmal ein paginiertes Template an. Da haben wir vor/zurück-Links, für die Google sich sehr interessieren würde. Um Google also ganz explizit zu erklären, dass die angezeigte Seite nur ein Teil des Inhalts darstellt, gibt es extra Angaben dafür: <a href="http://googlewebmastercentral.blogspot.de/2011/09/pagination-with-relnext-and-relprev.html">rel="prev" und rel="next"</a>.

Dazu definieren wir einen extra Block in der base.html:

Code:
...
<title>{% block pagetitle %}Seitentitel nicht gesetzt{% endblock %} - Blog</title>
{% block meta %}{% endblock %}
<style type="text/css">
...
Den wir in der category.html so befüllen:

Code:
{% block meta %}
{% if posts.has_previous %}
<link rel="prev" href="{{ category.get_absolute_url }}?page={{ posts.previous_page_number }}">
{% endif %}
{% if posts.has_next %}
<link rel="next" href="{{ category.get_absolute_url }}?page={{ posts.next_page_number }}">
{% endif %}
{% endblock %}
Wir könnten auch noch einen speziellen Wert für page festlegen (z.B. all), bei dem dann alle Posts ausgeben werden, um das bei der Angabe für canoncial anzugeben, aber da die Seite sehr groß werden würde, empfiehlt sich das nicht wirklich.

Diesen Teil kann man aber auch gut generisch handhaben und ähnlich wie show_comment.html auslagern.

Externe Dateien
Was bei unserem Mastertemplate immernoch fehlt, ist ein wenig Stil. Das HTML ist bereits vorbereitet für <a href="http://twitter.github.com/bootstrap/">Bootstrap</a> vorbereitet, sodass wir dieses nun integrieren möchten. Dafür benötigen wir allerdings ein paar externe Dateien. Auch hier haben die Django-Entwickler mitgedacht (ich auch, als ich vorhin sagte, dass wir das Verzeichnis template_static vorsorglich erstellen). Dafür gibt es das <a href="https://docs.djangoproject.com/en/1.4/ref/contrib/staticfiles/">System für statische Dateien</a>, welches standardmäßig aktiv ist und nur noch konfiguriert werden muss:

Code:
STATICFILES_DIRS = (
    "djangotut/templates_static",
    ...
)
Die gleiche Warnung wie bei TEMPLATE_DIRS gilt auch hier; jedenfalls können wir jetzt in diesen Ordner ein paar statische Dateien packen. Also entpacken wir Bootstrap in diesen Ordner und können es jetzt einfach referenzieren:

Code:
{% block meta %}{% endblock %}
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-responsive.min.css">
<style type="text/css">
In einer Produktivumgebung (zu all den Optimierungsmöglichkeiten dort kommen wir zum Schluss) würde man einfach alle URLs, die mit /static beginnen vom Webserver ausliefern lassen und mit entsprechenden Caching-Headern versehen. Hier erledigt das der Django-Entwicklungsserver erstmal für uns.

Nun sollte die Seite automagisch™ besser aussehen bzw. überhaupt irgendwie aussehen ;)

Fortsetzung folgt
 
Zuletzt bearbeitet:
Zurück
Oben