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:
- Ein Model für Posts, welches wir bereits erstellt haben
- 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:
- Die gesamte Datenbank löschen, das Model verändern und die Datenbank neu erzeugen (klingt blöd, ist blöd)
- 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)
- 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?
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:
- Die meisten Views geben Kram mit einem Template aus
- Die meisten Views nutzen dabei immer das gleiche Template
- 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
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:
- Eine Liste von Jahren ohne Doppeleinträge
- Die Anzahl der Zeilen/Posts für jedes Jahr
- 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