Start :: Magazin :: mac.delta-c mit Django – Das Making-of

mac.delta-c mit Django – Das Making-of

Nachdem acht Jahre lang mac.delta-c mit Drupal realisiert wurde, habe ich im Winter 11/12 einen Wechsel vollzogen. Heraus gekommen ist eine gemischte Installation von Django, Habari und dem Vanilla-Forum. In diesem making-of skizziere ich die Gründe für den Umstieg und umschreibe ein paar Details der Umsetzung.

Drupal 7 und eine fehlende API

Für die Aktualisierung des Weblogs, das nun unter dem Begriff Journal firmiert, hatte ich lange Zeit erst ecto und dann MarsEdit eingesetzt. Mit der jetzt aktuellen Version 7 von Drupal wurde indes die für MarsEdit benötigte Schnittstelle, die Blog API, aus der Standardinstallation von Drupal entfernt. Zwar kann man die Funktionalität mit einem Modul zur Zeit mehr schlecht als recht nachrüsten, aber diese Streichung war nur der Ausgangspunkt für ein paar weitere Überlegungen. Immerhin hatten sich in der alten Installation fast 1.500 einzelne Einträge und Artikel angesammelt, und deren Pflege und Aktualisierung sowie die Erstellung neuer Inhalte wurden immer mühsamer. Warum eigentlich?

Ein Machtverlust...

Zunächst galt es eine eher banale, aber oft nicht beachtete Tatsache nochmal neu zu durchdenken: Mit einem Content-Management-System verliere ich die Macht über meine Daten (hier: Texte) in der Hinsicht, als dass ich nicht selbst bestimmen kann, wie die Daten in dessen Datenbank organisiert werden. Es wird hier also mit der Eingabe der Texte Macht an das System abgegeben und in längerer Hinsicht hängt man fast schon auf Gedeih und Verderb von dessen weiterer Entwicklung ab. Nimmt es jedoch eine Richtung, die einem nicht gefällt oder ändern sich die Anforderungen, dann kann sich dies durchaus als Falle herausstellen. Nicht ohne Grund verdienen die Consultants da draußen, die sich schwerpunktmäßig mit der Migration von Datenbanken beschäftigen, gar nicht mal so schlecht. Im Fall einer doch eher simplen Webseite ist es möglicherweise nicht so dramatisch. Es scheint mir aber dennoch ein Problem zu sein, weil man den wesentlichen Bereich nicht mehr unter seiner Kontrolle hat.

Bei vielen der Content-Management-Systeme muss man immer wieder (Sicherheits-)Updates einspielen, um die Webseite verfügbar zu halten und auch die persönlichen Daten von Anwendern zu schützen, die sich etwa bei einem Diskussionsforum mit ihrer E-Mail-Adresse registriert haben. Bei Drupal stellte es sich am Ende für mich so dar, dass ich zunächst eine Reihe von weiteren Modulen und Erweiterungen zusätzlich zum eigentlichen Drupal-Kern installieren musste, um die von mir gewünschte Funktionalität zu erreichen. Am Ende waren es mehr als 30 Erweiterungen, von denen mindestens 20 für die eigentlichen Grundfunktionen der Webseite benötigt wurden. Jedes dieser installierten Module kann (theoretisch und praktisch) Sicherheitslücken, zum Beispiel in Bezug auf SQL-Injection, aufweisen, die die gesamte Webseite zerstören. Je mehr Module installiert wurden, desto höher ist diese Gefahr und desto höher ist ebenfalls der Zeitaufwand für das Einspielen der Updates. Bei meiner Drupal-Installation erschienen am Ende alle zwei bis vier Tage teilweise wichtige Sicherheitsupdates, die umgehend eingespielt werden mussten. Diese Zeit wäre eigentlich für das Schreiben vorgesehen.

Ein weiteres Problem bestand darin, dass die installierten Drupal-Module oft nur rund 80% der Funktionalität boten, die ich eigentlich gebraucht hätte. Diese Lücken lassen sich zwar stopfen, aber dies bedeutet einen nicht geringen Zeit- und Arbeitsaufwand. Die Ergebnisse basieren dann auf der Arbeit und den Konzepten anderer Leute. Das muss nicht immer schlimm sein, weil man ja von dieser Arbeit profitiert. Es stellt sich dennoch die Frage, inwiefern nicht eine Eigenentwicklung, die zu 100% den eigenen Vorstellungen entspricht, sinnvoller gewesen wäre. Bei komplexeren Aufgabenstellungen wie dem Podcast, der in den vorigen Version dieser Webseite viel Arbeitszeit benötigt hat, könnte sich die einmalige Investition für die Eigenentwicklung schnell rentieren.

Die Abhängigkeit von der Arbeit und auch Motivation anderer Menschen kann sich ferner als Danaergeschenk erweisen. Wenn die Entwickler der Module ihre Arbeit einstellen, was völlig in Ordnung ist, weil die meisten dieser Module und Erweiterungen in der Freizeit entwickelt werden, dauert die Suche nach einem Ersatz recht lange, sofern überhaupt ein adäquater Ersatz verfügbar ist. Das Modul, das für die Realisierung des kleinen Link-Katalogs zuständig war, habe ich mit jeder Drupal-Version austauschen müssen. Die Module wurden für die neue Drupal-Version nicht weiter entwickelt. Das bedeutete dann auch, die Daten per Hand (und in diesem Fall war es über die Zwischenablage) zu transferieren. Meines Erachtens eigentlich unproduktive Zeitverschwendung.

Und schließlich nervte mich am Ende auch die Tatsache, dass sich aufgrund der Datenstruktur die eigentlichen Texte mit HTML- & JavaScript-Anweisungen vermischten. Um diesen Vergrößerungs-Effekt bei den Bildern zu realisieren, befanden sich am Ende die notwendigen HTML-Tages im Text selbst. Damit kann ich beim Schreiben zwar umgehen, aber wenn das hinter der Funktion stehende JavaScript ausgetauscht wird und das neue JavaScript andere Tags benötigt, dann heißt es Suchen & Ersetzen im Fließtext. Bei einer MySQL-Datenbank ist das kein Spaß.

Unterm Strich sind das alles Probleme, die sich zwar lösen beziehungsweise beherrschen lassen, die aber dennoch Zeit, Arbeit und Energie kosten. Diese Zeit würde ich lieber in das Schreiben von Beiträgen und Artikeln stecken.

Bestandsaufnahme

Vielleicht einfach mal fünf bis zehn Schritte zurück und die Fragen neu gestellt: Wie will ich arbeiten? Welche Programme möchte ich für welche Zwecke einsetzen? Und ist es nicht einfacher, mir meine Lösungen selbst zu stricken und zwar exakt nach meinen Vorstellungen? Könnte es nicht sein, dass Eigenentwicklungen schneller, flexibler und wartungsfreundlicher sind als die CMS-Lösungen von der Stange? Was brauche ich eigentlich für Funktionen und welche Tools setze ich dafür ein? Wozu die eierlegende Wollmilchsau, die zwar alles irgendwie bietet, aber in Teilbereichen dann wieder sehr schnell an ihre Grenzen stösst?

Meine Antworten auf diese Fragen ließen sich in Anlehung an das Gebot der Sparsamkeit bei der Datenerhebung (Ja, ich weiß, der Datenschutz ist das Papier nicht wert, auf dem er gedruckt wurde, aber der Grundgedanke ist dennoch nicht falsch.) forumlieren: Je weniger Code auf dem Webserver ausgeführt wird, desto besser. Letztlich geht es doch »nur« darum, Hypertext im Browser darzustellen. Dynamik im Sinne von PHP- oder sonstigen Skripten wird nur dort eingesetzt, wo sie unbedingt benötigt wird. Das Einreichen von Fragen / Antworten bei einem Diskussionsforum wäre nun nicht gerade das Mittel der Wahl. Ein solcher Bereich müsste also dynamisch sein. Einfache Texte wie dieser hier können problemlos als einfache HTML-Datei auf dem Webserver gespeichert werden. Doch der Reihe nach:

Nachdem ich mir die alte Webseite nochmal genau angeschaut habe, kann ich auf zwei Funktionen verzichten. Zunächst ist es nicht mehr nötig, dass unter längeren Artikeln und Beiträgen, die nicht im Journal stehen, Kommentare erfolgen können. Da kam in der Vergangenheit selten etwas, das wirklich zum Thema beitrug. Nur zu oft wurden Kommentare abgegeben, die mit dem eigentlichen Inhalt des Textes nichts zu tun hatten. Es war meist offensichtlich, dass der Kommentierende ein wenig bei Google gesucht und ein völlig anderes Anliegen (kostenlose Problemlösung) hatte, dem gefälligst prompt begegnet werden sollte. Ebenfalls gestrichen wurde die Suchfunktion. Nach Durchsicht der Protokolle habe ich festgestellt, dass die fast gar nicht genutzt wurde.

Insgesamt werden also zwei Rubriken und ein Teilbereich des Designs dynamisch realisiert. Der Rest kann in Form statischer HTML-Dateien erfolgen, die aufgrund der Integration der RSS-Feeds über SimplePie mit der Dateiendung .php versehen werden. Damit dürfte die Wartung und Pflege der Webseite in Zukunft viel weniger Zeit und Aufwand erfordern. RSS-Parser sind nun nicht gerade für ihre Sicherheitslücken bekannt. Habari ist zwar noch etwas »bleedig edge«, aber der Code erscheint mir aufgeräumt, man könnte fast sagen abgeklärt. Viele Fehler, die anderweitig in finalen(!) Versionen für Ärger sorgen, wurden hier einfach nicht gemacht. Lediglich beim Forum werde ich genau aufpassen müssen. Nicht zuletzt ist dies auch der Bereich, in dem Benutzer Daten eingeben.

Schreiben mit...

Wenn nur zwei Bereiche dynamisch sind, dann können die Seiten zu den Büchern, das Magazin, die Testberichte, der Podcast, die Links und alle anderen Seiten statisch auf dem Server abgelegt werden. Das führt dann aber zur nächsten und eigentlich wichtigsten Frage: Wie und womit möchte ich schreiben? Meiner Meinung nach wird dieser Punkt viel zu oft übersehen. Dabei handelt es sich bei dem Schreiben um den zentral Vorgang, die Webseite ist lediglich das Medium. Während beim Forum wohl das Formular im Browser das Mittel der Wahl sein wird, kommt bei der Pflege des Journals wie gehabt MarsEdit zum Einsatz. Und womit schreibe ich die langen, die eigentlich wichtigen Texte? - Dabei ist es nicht nur das Schreiben an sich, sondern auch das Recherchieren und Informationen sammeln im Vorfeld, die Erstellungen und Benennung von Bildschirmfotos sowie die Verwaltung etwaiger Beispieldateien. Das gehört auch zum Arbeitsprozess.

Letztlich kommt für mich persönlich nur DEVONthink Pro in Frage. In einer DEVONthink-Datenbank kann ich Texte ablegen, weitere Dateien und Daten sammeln, darunter auch PDF-Dateien und umfangreiche Dokumentationen, die beim Schreiben hinzugezogen werden. So lassen sich alle Dateien in einem Programm zusammen fassen. Da DEVONthink über eine sehr gute AppleScript-Unterstützung verfügt, dürfte es später auch kein Problem darstellen, die Texte nebst Bildern und Metadaten (halb-)automatisch aus der Datenbank zu exportieren.

Ein kleiner Redaktionsserver mit Django

Und dann bleibt noch die Frage, wie ich eigentlich die HTML-Seiten auf meinem Rechner erstelle. Ordnerübergreifendes Suchen & Ersetzen ist im Jahr 2012 nun nicht gerade das Mittel der Wahl. Nun, für die lokale Berechung der Webseiten anhand einiger Templates ist Django mehr als ausreichend. Die enthaltene Template-Engine ist leistungsstark, mit Views und Controllern (MVC, anyone?) kenn' ich mich auch ein wenig aus und wenn ich weitere Funktionen benötige, können die schnell mit Python realisiert werden. Korrekturen der schon vorhandenen Texte können über die Admin-Oberfläche Da das System ausschließlich auf meinem Rechner läuft und keine Verbindung zum Internet herstellt, kann bei der Entwicklung auch etwas sorgenfreier gearbeitet werden. Auf Sicherheitslücken muss ich nicht achten, da am Ende nur HTML-Dateien mit der Endung .php gespeichert und auf den Webserver hochgeladen werden. Eigentlich stricke ich mir damit meinen eigenen, kleinen Redaktionsserver.

Die Installation und Einrichtung des Django-Systems werde ich an dieser Stelle nicht beschreiben. Ich hab' die MacPorts für Django und eine weitere Installation des Apache verwendet. Das ließ sich in einer Stunde vornehmen, ist aber nicht wirklich spannend. Und die Konfiguration ist in erster Linie auf mich zugeschnitten. Würde man ein solches System für mehrere Anwender oder eine Firma konfigurieren, dann müssten die Einstellungen anders vorgenommen werden. Nach der Einrichtung steht mit mein Django-System im Browser unter der Adresse http://Django auf meinem lokalen Rechner zur Verfügung.

Kleine Anmerkung: Die folgenden Erläuterungen sind möglicherweise unverständlich, wenn man nicht weiß, worum es sich bei Django handelt, welche Ziele dieses Framework verfolgt und welchen Zwecken es dient. Diese Einführung sowie der Eintrag in der Wikipedia helfen beim Verständnis.

Django - Ein paar Datenmodelle

Die einzelnen Felder für einen Inhaltstyp wie etwa einen Link oder einen Blogeintrag lassen sich unter Drupal mit dem Content Construction Kit definieren. Die grundlegenden Funktionalitäten dieses Moduls sind mit Version 7 Teil der Standardinstallation von Drupal geworden. Das hatte ich allerdings als eine recht mühselige Arbeit in Erinnerung. Es galt schon viele Klicks im Browser auszuführen, bis die einzelnen Felder für einen Inhaltstyp korrekt definiert wurden. In Django erstellt man das Datenmodell mit ein paar Zeilen Python-Code. Wer sich für die Details interessiert, dem bietet das Django Tutorial einen ersten Einblick. der Inhaltstyp für die Links ist vergleichsweise trivial und wird mit folgenden Zeilen realisiert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Weblink(models.Model):
    KATEGORIEN = (
        ('N', 'Nachrichten und Neuigkeiten (Deutsch)'),
        ('E', 'Nachrichten und Neuigkeiten (Englisch)'),
        ('A', 'Nachrichten und Neuigkeiten (Allgemein)'),
        ('W', 'Weblogs'),
        ('H', 'Hintergründe und Grundlagen'),
        ('F', 'Fun und nicht so Wichtiges'),
        ('V', 'Verzeichnisse'),
    )
    Kategorie = models.CharField(max_length=1, choices=KATEGORIEN)
    Headline = models.CharField(max_length=500)
    Beschreibung = models.CharField(max_length=1000)
    URL = models.CharField(max_length=1000)

Das Tuple KATEGORIEN definiert eben jene. Es wird dann in dem Feld Kategorie verwendet und führt zu einem Auswahlmenü. Ansonsten gibt's lediglich noch die Headline, also quasi den Titel, die Beschreibung und den eigentlichen Link. Bei allen Feldern werden einfach nur Zeichen eingegeben. In der Verwaltungsoberfläche von Django steht ohne weiteres Zutun ein Formular zur Verfügung, über das ein neuer Link samt Beschreibung und Kategorie eingegeben werden kann.

Bei den längeren Beiträgen im Magazin wird die Sache dann etwas kniffliger. Um die Webseite als solche zugänglicher zu machen, benötigen die Beiträge etwas mehr Felder als einfach nur einen Haupttext und eine Überschrift. Dazu gehört zum Beispiel der Teaser, der auf der Startseite und bei der Auflistung der Rubrik angezeigt wird und den Inhalt des Beitrags zusammen fasst. Auch soll jedem Beitrag eine kleine Grafik (Icon) zugegeben werden, die ebenfalls in der Auflistung erscheint. Das Feld Iusethis speichert einen Lik auf einen Eintrag im gleichnamigen Dienst, im Feld SlugURL wird der URL für den Beitrag gespeichert. Zu einem Beitrag gehören in der Regel eine unbekannte Anzahl von Abbildungen. Für diese Abbildungen wird ein eigener Beitragstyp definiert. Eine Abbildung besteht aus dem eigentlichen Bild, dem größenmäßig angepassten Bild, einer eindeutigen Bezeichnung sowie einer Beschriftung. Zusätzlich werden noch die Höhe und Breite angegeben. Da jedem Beitrag beliebig viele Abbildung zugeordnet werden können, muss hier eine 1:n-Beziehung hergestellt werden. Dies geschieht in der Klasse Abbildung über das Feld Beitrag mittels models.ForeignKey(Beitrag).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Beitrag(models.Model):
    ART = (
        ('M', 'Magazin'),
        ('T', 'Test & Kauf'),
    )
    Kategorie = models.CharField(max_length=1, choices=ART)
    Datum = models.DateField(auto_now_add = True)
    Headline = models.CharField(max_length=300)
    ShortHeadline = models.CharField(max_length=300)
    Teaser = models.TextField()
    Inhalt = models.TextField()
    InhaltHTML = models.TextField()
    Iusethis = models.CharField(max_length=200, blank=True)
    SlugURL = models.SlugField(max_length=50, blank=True)
    Icon = models.ImageField(upload_to="bilder", height_field='Bildhoehe', width_field='Bildbreite')
    Bildhoehe = models.IntegerField(null=True)
    Bildbreite = models.IntegerField(null=True)
    def __unicode__(self):
        return self.Headline
    def save(self):
        self.InhaltHTML = filter.MakeHTML(self,self.Inhalt)
        super(Beitrag, self).save()

class Abbildung(models.Model):
    Beitrag = models.ForeignKey(Beitrag)
    Bild = models.ImageField(upload_to="bilder")
    Bild_tn = models.ImageField(upload_to="bilder", height_field='Bildhoehe', width_field='Bildbreite')
    Bildhoehe = models.IntegerField(null=True)
    Bildbreite = models.IntegerField(null=True)
    Beschriftung = models.CharField(max_length=300)
    Bezeichnung = models.CharField(max_length=30)

Dann werden bei den Beiträgen noch zwei Funktionen definiert. Die erste def unicode(self): sorgt lediglich dafür, dass die Überschrift (Headline) des Beitrags in der Verwaltungsoberfläche von Django erscheint. Die zweite Funktion (def save(self)) kommt später bei den Filtern ins Spiel. Vorweg: Sie dient dazu, dass beim Speichern des Beitrags automatisch eine zweite Version des Haupttexts über die Funktion MakeHTML in der Datei filter.py erzeugt wird. Es wird dabei die standardmäßige Funktion save überschrieben, was auch entsprechend dokumentiert ist.

Die Seiten zu den Büchern sind eine etwas abgespeckte Variante der Beiträge. Auf Bilder wird hier verzichtet, und es wird für jedes Buch eine eigene Kategorie erstellt, unter der dann die einzelnen Seite (Inhaltsverzeichnis, Pressestimmen, etc.) abgelegt werden.

Django - Ein paar Views und Templates

Django folgt, mehr oder weniger hemdsärmelig, dem MVC-Muster. Um nun die zuvor definierten Datenmodelle im Browser anzeigen zu können, braucht es also erstmal ein View. Zuvor muss noch mit ein wenig Regex die URL definiert werden. So führt die Zeile

1
url(r'^magazin/(?P<SlugURL>(.*?))/$', 'kaimain.views.magazin_details'),

in der Datei urls.py dazu, dass ein Aufruf von http://mac.delta-c.de/magazin/name-des_artikels an die Funktion magazin_details weitergeleitet wird. Der in spitzen Klammern aufgeführte Wert SlugURL wird dabei übergeben, und für eine Datenbankabfrage genutzt, um den Beitrag auszugeben, der über den Eintrag SlugURL (s.o.) identifiziert wird. Für die Übersichtsseite des Magazins wird folgendes Muster verwendet:

1
url(r'^magazin/$', 'kaimain.views.magazin_overview'),

Ruft ein Surfer also die URL http://mac.delta-c.de/magazin auf, dann wird die Funktion magazin_overview ausgeführt. Diese in der Datei views.py definierte Funktion hat folgenden Aufbau:

1
2
3
4
5
6
7
def magazin_overview(request):
    Vorlage = loader.get_template('kaimain/magazin/index.html')
    Beitragsliste = Beitrag.objects.filter(Kategorie='M').order_by('-Datum')
    Inhalte = Context ({
        'Beitragsliste' : Beitragsliste
    })
    return HttpResponse(Vorlage.render(Inhalte))

Mag auf den ersten Blick etwas kryptisch wirken, ist aber simpel: Zunächst nimmt die Funktion den HTTP-Request entgegen, und lädt zunächst eine noch zu erstellende Vorlage index.html aus dem Verzeichnis kaimain/magazin. Dann erfolgt eine Datenbankabfrage, bei der die vorhandenen Objekte der zuvor definierten Kategorie M für das Magazin ausgelesen werden, und zwar nach Datum in absteigender Reihenfolge sortiert. Diese Beitragsliste wird dann mittels Context an die Vorlage übergeben und dann das ganze Konstrukt berechnet.

Damit diese Berechnung stattfinden kann, braucht es noch die Vorlage index.html. Diese besteht nicht aus einem vollständigen HTML-Dokument, sondern greift auf die Hauptvorlage default.html im obersten Verzeichnis der Template-Struktur zurück. Für die Darstellung der unter M abgelegten Beiträge dient folgender Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends "default.html" %}

{% block content %}
    <h1>Magazin</h1>
    <p>
    In diesem Magazin stelle ich Artikel zusammen, die ein Thema etwas umfangreicher behandeln.
    Dabei kann es sich um ein Tutorial, eine umfangreiche Programmvorstellung mit verschiedenen
    Einsatzszenarien oder ein allgemeines Thema handeln. Neue Artikel erscheinen mehr oder 
    weniger regelmäßig.
    </p>
    {% if Beitragsliste %}
        {% for Beitrag in Beitragsliste %}
        <p class="ShortHeadline">{{ Beitrag.ShortHeadline }}</p>
        <h2><a href="/magazin/{{ Beitrag.SlugURL }}">{{ Beitrag.Headline }}</a></h2>
        <p><img src="/static/media/{{ Beitrag.Icon }}" width="{{ Beitrag.Bildbreite }}" 
        height="{{ Beitrag.Bildhoehe }}">
        <b class="grau">{{ Beitrag.Datum }} &bull; </b>
        {{ Beitrag.Teaser|safe }}</p>
        <p><a href="/magazin/{{ Beitrag.SlugURL }}">Weiterlesen &gt;&gt;&gt;</a></p>
    {% endfor %}
    {% endif %}
{% endblock %}

Die hier nicht dargestellte Datei default.html enthält das eigentliche Design der Webseite. An einigen Stellen wurden Blöcke in der Form {% block content %}{% end block %} als Platzhalter eingefügt. Mit der ersten Zeile {% extends "default.html" %} wird dieses Haupttemplate quasi geladen und ergänzt. Der mit Content bezeichnete Platzhalter befindet sich an der Stelle, an der der eigentliche Inhalt eingefügt wird. Innerhalb dieses Blocks wird dann in einer Schleife {% for Beitrag in Beitragsliste %} die übergebene Liste der Beiträge abgearbeitet und für jeden Beitrag der entsprechende HTML-Code erzeugt. Angaben wie {{ Beitrag.Headline }} sorgen dafür, dass der Inhalt des Felds an dieser Stelle eingefügt wird.

An und für sich war's das eigentlich, was die Darstellung der Beiträge im Browser angeht. Natürlich müssen für die einzelnen Übersichten (Magazin, Test & Kauf, Startseite, Links, etc. pp.) die URL-Muster festgelegt, die Funktionen der Views entwickelt und die entsprechenden Vorlagen angelegt werden. Das ist eher Fleißarbeit, die aber passgenau erfolgen kann. Bei der Erstellung der Templates gibt es keine technischen Begrenzungen. Es können alle Funktionalitäten genutzt werden, die Django für diese Zwecke bietet.

Für individuelle Ansichten bietet Drupal ein Views genanntes Modul.

Für individuelle Ansichten bietet Drupal ein Views genanntes Modul.

Der Ansatz von Django, eine Darstellung im Browser über die Python-Funktionen in den Dateien urls.py und views.py zu erzielen, wirkt auf den ersten Blick oft etwas kompliziert. Im direkten Vergleich zu dem Views-Modul von Drupal ist der Ansatz von Django aber eine regelrechte Wohltat. Während ich in Drupal aufgrund der nur noch esoterisch zu nennenden Oberfläche des Moduls mehr als einmal fast verzweifelt wäre, kann ich Django mit Python-Code exakt die Ansichten definieren.

Django - Ein paar Tricks und Filter

Damit steht das Grundgerüst der Webseite bereits. Beiträge können erstmal über den Browser eingegeben und auch wieder betrachtet werden. Beim jetzigen Stand müssen Texte aber noch mit HTML-Formatierungen eingegeben werden, die Abbildungen werden noch nicht verwaltet, die Erzeugung statischer Seiten ist noch nicht realisiert und ein Import aus DEVONthink findet ebenfalls noch nicht statt.

Zunächst braucht es eine Alternative zu HTML als Auszeichnungssprache. Mit spitzen Klammern zu schreiben ist insbesondere bei einer deutschen Tastaturbelegung nicht gerade erfreulich. Für dieses Problem gibt es mittlerweile eine Vielzahl von mehr oder weniger eleganten Lösungen. Eine davon ist Markdown und was für John Gruber Recht ist, soll mir nur billig sein. Ein passendes Python-Modul lässt sich problemlos über die MacPorts nachinstallieren. Nun kommt auch die eingangs definierte Funktion def save(self) zum Einsatz. Diese dient dazu, den Inhalt des Beitrags beim Speichern entgegen zu nehmen und zu verarbeiten. Das Ergebnis wird dann im Datenbankfeld InhaltHTML (s.o.) gespeichert. Letzteres enthält den eigentlichen HTML-Code und wird für die Ausgabe im Browser verwendet. Das Feld Inhalt enthält den ursprünglichen Text, der mittels Markdown ausgezeichnet wurde. Die Funktion save in einer separaten Datei filter.py hat zunächst folgenden Aufbau:

1
2
3
4
5
6
7
# -*- coding: UTF-8 -*-
import models
import markdown

def MakeHTML(Beitrag,Text):
    Text = markdown.markdown(Text)
return Text

Das ist schon ausreichend, um die Auszeichnungen mittels Markdown in valides HTML zu überführen. Das Markdown-Modul verfügt allerdings noch über Erweiterungen und ist in der Lage, auf das exzellente Modul Pygments zurückzugreifen. Daher wird die zur Zeit einzige Zeile der Funktion MakeHTML nach der Installation von Pygments über die MacPorts folgendermaßen abgeändert:

1
    Text = markdown.markdown(Text, ['codehilite(force_linenos=True)'])

Ab jetzt werden Absätze, die mit vier Leerzeichen eingerückt wurden, dem Syntax Higlighting unterzogen. Zu Begin eines eingerückten Code-Blocks kann über drei Doppelpunkte und einem Kürzle dann die Programmiersprache vorgegeben werden. Wenn die drei Zeilen

:::python
#!/usr/bin/python
print ("Hallo Welt")

mit jeweils vier Leerzeichen eingerückt werden, dann erscheint folgendes Listing:

1
2
#!/usr/bin/python
print ("Hallo Welt")

Im Klartext: Durch die Installation zweier Python-Module und der Abwandlung einer (!) Zeile in einer Funktion wird das Syntax-Highlighting automatisch vorgenommen. Gerade beim Schreiben von Texten, die wie dieser viel Quellcode beinhalten, eine unschätzbare Arbeitserleichterung. Und für den Leser ein Gewinn an Übersichtlichkeit.

Offen geblieben ist die Frage, wie die Abbildung im Text platziert werden können. Im Datenmodell wurde ja schon ein Typ definiert. Im Fließtext wird die Stelle, an der eine Abbildung eingefügt werden soll, durch Angabe der Bezeichnung der Abbildung in spitzen Klammern markiert, also beispielsweise <Bildchen>. Über einen einfachen regulären Ausdruck wird dann an dieser Stelle der HTML- und JavaScript-Code eingefügt, der sowohl die Beschriftung als auch die Vergößerungsfunktion mittels jQuery beinhaltet. Sollte ich zu einem späteren Zeitpunkt das JavaScript für die Vergrößerung auswechseln, dann muss lediglich die beim Suchen & Ersetzen genutzte Vorlage für die Bidler angepasst werden. Der ganze Filter, der beim Speichern eines Textes ausgeführt wird, hat nun folgenden Aufbau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding: UTF-8 -*-
import re
from string import Template
import models
import markdown

def MakeHTML(Beitrag,Text):
    Text = markdown.markdown(Text, ['codehilite(force_linenos=True)'])
    Fundstellen = re.finditer(r'\<(?P<Bezeichner>(.*?))\>', Text)
    Vorlage = Template("""<div class="bild"><a href="/static/media/$DATEINAME"
        rel="lightbox" title="$BESCHRIFTUNG"><img src="/static/media/$DATEINAMETN"
        border="0" alt="$BESCHRIFTUNG" width="$BREITE" height="$HOEHE"></a>
        <p>$BESCHRIFTUNG</p></div>""")
    for Fundstelle in Fundstellen:
        Bezeichner = Fundstelle.group('Bezeichner')
        try:
            Bild = models.Abbildung.objects.get(Beitrag=Beitrag,Bezeichnung=Bezeichner)
            Ersetzungen = dict (DATEINAME=Bild.Bild,
                DATEINAMETN=Bild.Bild_tn,BESCHRIFTUNG=Bild.Beschriftung, BREITE=Bild.Bildbreite, 
                HOEHE=Bild.Bildhoehe)
            Endcode = Vorlage.substitute(Ersetzungen)
            AlterText = "\<" + Bezeichner + "\>"
            Text = re.sub(AlterText, Endcode, Text)
        except:
            continue
    return Text

Dass da am Ende in eher nonchalanter Art und Weise ein Fehler mittels except ... continue ignoriert wird, möge die geneigte Leserschaft bitte übersehen. Übergangsweise ist das aber schon ausreiched. Anstelle einer Markierung wie <Bildchen> wird dann der in der Variable Vorlage definierte HTML-/JavaScript-Code eingefügt. Das ist sicherlich noch ein Bereich, der weiter zu optimieren wäre, aber für's Erste soll's mal reichen.

Zum Schreiben soll DEVONthink genutzt werden. Um DEVONthink und die Django-Installation miteinander zu verheiraten, sind zwei Dinge notwendig. Zunächst wird das Format für einen Waschzettel definiert. Dieser enthält die Metadaten des Beitrags, wie die Überschrift, das Logo, die Kategorie und das Icon. Für das Format dieser Datei wird der ConfigParser genutzt. Es entspricht damit dem vielleicht von Windows schon bekannten Schema. Die Datei Config.txt für diesen Beitrag hat folgenden Aufbau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Einstellungen]
    Kategorie = M
    Headline = mac.delta-c mit Django – Das Making-of
    Shortheadline = Web-Work Django
    Icondatei = makingof.png
    Hauptdatei = Making.txt
    Teaser = Fast acht Jahre lang habe ich für mac.delta-c eine Drupal-Installation 
    eingesetzt. Im Dezember 2011 hatte ich dann die aktuellen Auflagen zu OS X
    10.7 fertig gestellt. Anschließend wollte ich mich dann der Renovierung der
    Webseite widmen. Dabei kam ich schnell zu dem Schluss, dass die weitere
    Arbeit mit Drupal nicht mehr meinen Anforderungen entspricht. Warum und
    wie der Wechsel auf eine Mischinstallation von Django, Habari und dem
    Vanilla-Forum erfolgte, erläutert dieses Making-of.

In DEVONthink wird für jeden Beitrag ein Ordner in der Datenbank angelegt. Der Ordner enthält die Datei Config.txt mit den obigen Metadaten, den über Hauptdatei identifizierten Haupttext, eine Icondatei sowohl die Abbildungen. Zu jeder Abbildung wird die Beschriftung in einer separaten Textdatei mitgegeben. Wenn es also eine Abbildung Symbolleiste.png gibt, dann befindet sich die Beschriftung in der Datei Symbolleiste.png.txt. Die Bezeichnung der Abbildung lautet dann Symbolleiste. Der Vorteil dieses Verfahrens besteht darin, dass DEVONthink die Seitenleiste im Finder um einen Inbox genannten Ordner erweitern kann. Dieser stellt den Zugang zum Ordner Eingang in der DEVONthink-Datenbank dar. Verschiebe ich eine Bilddatei im Finder in diesen Ordner, dann erscheint die Abbildung sofort in der DEVONthink-Datenbank und kann dort abgelegt und mit einer Beschriftung versehen werden. Mag vielleicht den einen oder anderen nicht beeindrucken, aber wer mal mit richtig vielen Bildschirmfotos zu tun hatte, der wird mich wohl verstehen.

Ein Beitrag wird nebst Metadaten und Abbildungen in einem Ordner in DEVONthink gespeichert.

Ein Beitrag wird nebst Metadaten und Abbildungen in einem Ordner in DEVONthink gespeichert.

Zum Import einer solchen Ordnerstruktur wird diese zunächst über den Menüpunkt AblageExportieren in einen Ordner innerhalb der Django-Installation kopiert. Zunächst liegen die Dateien dort erstmal nur rum. Zum Import dient ein separates Python-Skript, das diesen Ordner ausliest, die Dateien auswertet und die Einträge in der Datenbank erzeugt. Dieses Import-Skript umfasst derzeit rund 70 Zeilen. Es verkleinert mithilfe von ImageMagick die Abbildungen, sofern die Ausgangsdatei höher als 500 Pixel oder breiter als die 700 Pixel für diesen Inhaltsbereich sind. Manuelles Erstellen von Thumbnails entfällt.

Importieren und Verwalten funktioniert, es bleibt nun nur noch der Export in statische Dateien. Für diesen Zweck gibt es den StaticGenerator for Django. Nachdem dieses Modul installiert und konfiguriert wurde, speichert die Django-Installation beim Aufruf einer Seite diese im konfigurierten Verzeichnis auf der Festplatte. Der einzige wirkliche Hack ist dann in Zeile 177 der Datei init.py vorzunehmen. Hier muss die Endung der erzeugten Dateien von html auf php geändert werden. Wird nun eine Webseite in der Form http://Django/magazin/ein-beitrag aufgerufen, dann erstellt der StaticGenerator einen Ordner magazin mit einem Unterordner ein-beitrag. In letzterem findet sich dann eine Datei index.php. Ein Abruf über HTTP führt also zum Export des Beitrags in einer vollständigen HTML-Datei mit der Endung .php. Diese Ordner können dann auf den Webserver hochgeladen werden. Um diesen Export noch ein wenig zu vereinfachen, wurde ein Python-Skript geschrieben, dass aus der Datenbank alle Beiträge ausliest, mittels der urllib Pythons eine Abfrage erzeugt und somit die Beiträge en passant exportiert. Das erscheint ein wenig verwirrend, erst quasi von innen die Datenbank abfragen und dann von außen einen HTTP-Request erzeugen, funktioniert aber problemlos und führt zum gewünschten Ergebnis.

Und der Podcast?

Spätestens beim Podcast zeigt sich, wenigstens für mich, dass die bisherigen etwas verquer anmutenden Lösungen und Kombinationen flexibler sind als die Systeme von der Stange. Ich kenne kein ausgereifstes CMS, das eine schlanke und flexible Podcast-Lösung zur Verfügung stellt. In der alten Drupal-Installation wurde zur Erzeugung des iTunes-konformen RSS-Feeds ein Modul, eine Ansicht mittels des View-Moduls und weitere unterstützende Module und Skripten benötigt. Wenn ich mich richtig entsinne, habe ich zwei bis drei Arbeitstage gebraucht, bis die Lösung einigermaßen zufriedenstellend funktionierte. An dieser Stelle zeigt sich dann auch die eingangs beschriebene Problematik: Da man im CMS nicht mehr Herr der Daten(-strukturen) ist, stösst man bei Spezialfällen dann an die Grenzen. Indes handelt es sich bei einem Podcast um eine eigentlich triviale Funktion. Es wird lediglich eine Beschreibung, ein Dateianhang sowie ein entsprechend angepasster RSS-Feed benötigt. Also gilt es zunächst das Datenmodell für die Podcast-Beiträge zu entwerfen. Das ist ziemlich simpel:

1
2
3
4
5
6
class Podcast(models.Model):
    Headline = models.CharField(max_length=500)
    Beschreibung = models.TextField()
    Datum = models.DateTimeField()
    Datei = models.FileField(upload_to="podcast/")
    SlugURL = models.SlugField(max_length=20, blank=True)

Dürfte eigentlich selbsterklärend sein. Es wird ein Eintrag für die URLs benötigt, der dazu führt, dass die Abfrage von /podcast/ zur Übersicht der Episoden führt. Dann braucht es ein Template für diese Rubrik, das nur die obigen Einträge darstellen muss. Das war dann auch der Punkt, an dem die eigentliche Arbeit anfing. Wobei dies mit Django nichts zu tun hatte, vielmehr ging es um den Player, der für das Abspielen einer Episode im Browser genutzt wird. Ich hatte mal auf Google+ nachgefragt, ob jemand einen guten, in JavaScript realisierten Audio-Player kennt. Verwiesen wurde ich auf diese Liste. Die Bewertung »sucks less than the others« legt beredtes Zeugnis ab. Nachdem ich ein wenig mit einigen der aufgeführten Lösungen experimentiert hatte, habe ich mich dann für den jPlayer entschieden. Die Integration des Players in das Design und generell die Seitenstruktur mithilfe eines Templates war dann ziemlich einfach. Konflikte mit schon vorhandenen JavaScripten oder CSS-Klassen sind nicht aufgetreten, da die JavaScripten und CSS-Klassen handverlesen waren. Am Rande: Bei der alten Drupal-Installation hatte ich am Ende 18 CSS-Dateien und 12 bis 15 JavaScript-Bibliotheken. Deren Coexistenz war bisweilen alles andere als harmonisch. Und Fehlersuche in diesem Bereich ist [Kraftausdruck].

Ok, Tondateien werden zusammen mit der Überschrift und der Beschreibung im Browser angezeigt, fehlt nur noch der spezielle RSS-Feed für den Podcast. Ansonsten ist es Essig mit der iTunes-Integration. Django bringt von Haus aus schon eine ausgereifte RSS-Funktion mit. Damit sich der Standard-Feed gut in iTunes integriert, muss er noch etwas angepasst werden. Basierend auf einem frei verfügbaren Django Snippet werden zwei Python-Klassen erstellt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding: UTF-8 -*-
from kaimain.models import Podcast
from Django.utils.feedgenerator import Rss201rev2Feed
from Django.contrib.syndication.views import Feed

class PodcastFeedGenerator(Rss201rev2Feed):
    def rss_attributes(self):
        return {"version": self._version, "xmlns:atom": "http://www.w3.org/2005/Atom", 
            'xmlns:itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
    def add_root_elements(self, handler):
        super(PodcastFeedGenerator, self).add_root_elements(handler)
        handler.addQuickElement('itunes:explicit', self.feed['iTunes_explicit'])
        handler.addQuickElement('itunes:author', self.feed['iTunes_author'])
    def add_item_elements(self,  handler, item):
        super(PodcastFeedGenerator, self).add_item_elements(handler, item)
        handler.addQuickElement('itunes:author', 'mac.delta-c')

class PodcastFeed(Feed):
    feed_type = PodcastFeedGenerator
    title = "mac.delta-c | Der Podcast zur Webseite zu den Buechern"
    link = "/podcast/"
    description = "Hier kommt dann die Beschreibung rein."
    def items(self):
        return Podcast.objects.order_by('-Datum')[:5]
    def item_title(self, item):
        return item.Headline
    def item_link(self, item):
        return item.SlugURL
    def author_name(self):
        return "Kai Surendorf"
    def item_author_name(self):
        return "Kai Surendorf"
    def item_enclosure_url(self, item):
        Dateiadresse = "http://mac.delta-c.de/static/media/" + str(item.Datei)
        return Dateiadresse
    def item_enclosure_mime_type(self):
        return "audio/aac"
    def item_pubdate(self, item):
        return item.Datum
    def item_description(self, item):
        return item.Beschreibung
    def feed_extra_kwargs(self, obj):
        extra = {}
        extra['iTunes_explicit'] = 'yes'
        extra['iTunes_author'] = 'Kai Surendorf'
        return extra

Abschließend noch in der Datei urls.py die Abfrage von /podcast/podcast.xml auf eben die Klasse PodcastFeed() umleiten, und der Podcast ist fertig. Kurzum: Für die Realisation des Podcasts ging weniger als ein halber Nachmittag drauf, und die meiste Zeit für den Test der JavaScript-Player.

Und? Fazit?

Gut, mehr als 40.000 Zeichen für diesen Artikel. Und was will einem der Autor damit sagen? Ist das nun besser als Drupal oder Wordpress oder Typo3? - Es kommt halt darauf an. Akkurat Buch über die investierten Arbeitsstunden habe ich nicht. Würde aber im direkten Vergleich zum letzten kompletten Re-Design mit Drupal auf jeden Fall sagen, dass ich für die Neuentwicklung des Django-Systems entschieden weniger Zeit benötigt habe. Gerade die Arbeit mit den Drupal-Modulen Content Construction Kit und Views habe ich als nervig, unpräzise und fehleranfällig in Erinnerung. Mit Django brauche ich für diese Aufgaben nur eine Handvoll Zeilen Python-Code.

Das System ist sicher noch nicht fertig, und an einigen Stellen gilt es noch zu basteln, zu erweitern und auszuprobieren. Vom eigentlichen Schreiben dürfte mich dies nicht abhalten, da diese Erweiterungen oder Optimierungen nicht zwingend notwendig sind. Sie können dann vorgenommen werden, wenn mir danach ist. In der Zwischenzeit ist die vorhandene Lösung ausreichend stabil, um schnell Texte online zu stellen. Und das Journal mittels MarsEdit zu befüllen, ist eh mit minimalen Widerständen verbunden.

Mein Django-System ist sicherlich nichts, dass einem Kunden im Sinne von »Hier, da, mach mal allein weiter!« vorgesetzt werden kann. Umgekehrt würde ich aber in jedem Fall behaupten, dass sich die Einarbeitung in ein Framework wie Django dann lohnt und auch zeitlich rentiert, wenn man eine Programmiersprache wie Python eingiermaßen beherrscht. Durch die Unabhängigkeit und damit verbunden die Flexibilität wird viel Zeit gewonnen, die in die inhaltliche Weiterentwicklung zu investieren ist. Und nicht zuletzt hat mir dieses Gebastel und Gefrickel, das eigentlich keines ist, viel Spaß gemacht. Andere Meinungen? - Im Forum ist Platz dafür.

 

twitter und RSS

       

Verbraucherinformationen