Als ich vor einiger Zeit den Artikel zu Blog-Features in Django geschrieben habe, waren natürlich die meisten Sachen an dieses Blog und meinen Erfahrungen mit ihm angelehnt. Damals gab es noch keine Möglichkeit Trackbacks an mein Blog zu schicken. Das ist mittlerweile anders und deshalb kann ich nun auch erklären wie es funktioniert, außerdem hat mich der Martin per Mail um diesen Artikel gebeten.
Da man zwangsläufig die Trackback-Funktionalität in Django selber schreiben muss ist es ganz gut vielleicht im Vorfeld einmal einen Blick in die Trackback-Spezifikation zu werfen. Erfunden wurden die Trackbacks von den Leuten von SixApart für ihre Blogging-Software MovableType. So wissentlich gestärkt ist die Implementierung der Trackbacks eigentlich ziemlich einfach.
Spezifikationen
Eigentlich ist ein Trackback nichts anderes als eine normale HTTP-Anfrage, die den Sinn hat das eigene Blog über die Verlinkung oder das "Bezug nehmen" auf einen bestimmten Beitrag zu informieren. Dementsprechend muss es für jeden einzelnen Beitrag eine eigene Trackback-Adresse geben, damit das eigene/empfangende Blog den Trackback auch einem Beitrag zuordnen kann. Diese besondere URL nennt sich Trackback-Ping-URL. Das Informieren eines Quellblogs nennt sich auch Ping.
Das pingende Blog sendet den Ping ganz einfach in der Form eines HTTP-POST-Request an die Trackback-Ping-URL. Dementsprechend hat man in Django also fast nichts anderes zu tun als eine einfache Formularbehandlung.
Folgende Parameter kann das pingende Blog mitschicken:
title
Mit dem Namen des Beitrags in dem man sich das pingende Blog auf den Trackback-Beitrag bezieht.
excerpt
Ein kurzer Ausschnit aus dem Beitrag in dem sich auf den Trackback-Beitrag bezogen wird.
url
Die URL des bezugsnehmenden Beitrags.
blog_name (optional)
Enthält den Namen des Blogs welches den Trackback sendet.
Abweichend von einer einfachen Auswertung von POST-Parametern ist die Antwort, die das gepingte Blog (also in diesen Fall Django) senden muss. Dabei sieht die Spezifikation zwei Antworttypen vor, jeweils als einfaches XML.
Bei erfolgreichem Ping und korrekter Annahme des Trackbacks:
<?xml version="1.0" encoding="utf-8"?>
<response>
<error>0</error>
</response>
Dementsprechend bei einem Fehler und fehlgeschlagendem Ping:
<?xml version="1.0" encoding="utf-8"?>
<response>
<error>1</error>
<message>The error message</message>
</response>
Implementierung in Django
Übersetzt man die Spezifikation in Django-Sprache bedeutet das folgendes: Man muss eine spezielle URL anlegen, die für jeden Beitrag als Trackback-URL existiert. Der hinter dieser URL liegende View muss die oben beschriebenen Daten entgegen nehmen und als Trackback speichern. Man brauch also ebenfalls ein extra Model um die Trackbacks zu speichern. Danach muss der View eine Antwort generieren, die je nach Ergebnis die entsprechende XML-Antwort gibt.
Zuerst ist es also sinnvoll das folgende Model 'Trackback' in der models.py anzulegen und Django entsprechend die passende Tabelle dazu anlegen zu lassen:
class Trackback(models.Model):
blog = models.CharField("Blog",maxlength=200, core=True)
title = models.CharField("Titel",maxlength=200, core=True)
url = models.CharField("URL",maxlength=200, core=True)
excerpt = models.TextField("Auszug",core=True)
pub_date = models.DateTimeField("Datum",'date published')
post = models.ForeignKey(Post,related_name='trackbacks')
def __str__(self):
return "%s" % (self.title)
class Meta:
ordering = ['-pub_date']
verbose_name = "Trackback"
verbose_name_plural = "Trackbacks"
class Admin:
list_display = ('blog', 'url', 'post', 'pub_date')
date_hierarchy = 'pub_date'
list_filter = ('pub_date','post')
Dabei verknüpft der ForeignKey den Trackback entsprechend mit dem Model, dass man für seine Posts verwendet.
Nun legt man die Trackback-URL in seiner urls.py an und weist ihr eine entsprechende View-Funktion zu. Das könnte z.B. so aussehen:
urlpatterns = patterns('blog.views',
...
(r'^post/(?P<url_string>[0-9a-z_-]+)/$', 'post'), //URL für einen Post
(r'^post/(?P<url_string>[0-9a-z_-]+)/trackback/$', 'trackback'), //Trackback-URL
...
)Nun fehlt eigentlich nur noch die entsprechende View-Funktion in der views.py um den ankommenden Ping entsprechend auszuwerten. Diese lässt sich natürlich beliebig kompliziert anlegen. Mit Akismet oder ohne, wobei ich energisch empfehle immer Akismet mit einzubauen. Man ahnt nicht wie schnell man sich sonst Spam einfängt. Ich habe die Integration von Akismet im Blog-Features-Beitrag beschrieben, und dementsprechend verwendet auch der folgende Code Akismet.
def trackback(request, url_string):
try:
//Erstmal den Post finden
post = Post.objects.get(url_string__iexact=url_string)
except Post.DoesNotExist:
//Wenn man ihn nicht finden konnte sollte man dem pingenden Blog das sagen
return render_to_response('trackback-answer.xml',{'error':'1','errormsg':'Post not found, maybe you have the wrong trackback URL!'})
param = ''
try:
//Dann die Parameter auslesen
param = 'title'
title = request.REQUEST[param]
param = 'url'
url = request.REQUEST[param]
param = 'excerpt'
excerpt = request.REQUEST[param]
//das ist eigentlich unschön hier, weil der Parameter blog_name eigentlich optional ist
//naja, mir sei etwas schludern erlaubt :)
param = 'blog_name'
blog = request.REQUEST[param]
try:
//Nun kommt zuerst der Check mit Akismet ob der Trackback Spam ist
real_key = akismet.verify_key(AKISMET_API_KEY,BLOG_URL)
if real_key:
is_spam = akismet.comment_check(AKISMET_API_KEY,BLOG_URL,
request.META['REMOTE_ADDR'], request.META['HTTP_USER_AGENT'],
comment_author=blog,comment_content=excerpt,comment_type='trackback',comment_author_url=url)
if is_spam:
//Sollte er Spam sein, dann sollte man das wiederum dem pingenden Blog mitteilen
return render_to_response('trackback-answer.xml',{'error':'1','errormsg': 'Your Trackback is spam!' })
else:
//wenn alles OK ist dann den Trackback entsprechend speichern
trackback = Trackback()
trackback.blog = blog
trackback.title = title
trackback.url = url
trackback.excerpt = excerpt
trackback.pub_date = datetime.now()
trackback.post = post
trackback.save()
//und natürlich mit OK antworten
return render_to_response('trackback-answer.xml',{'error':'0'})
except akismet.AkismetError, e:
//Wird einfach nur geloggt, mehr kann man da nicht tun...
error("%s - %s" % (e.response, e.statuscode))
//Bescheid sagen
return render_to_response('trackback-answer.xml',{'error':'1','errormsg': 'Internal error occured! Something is wrong with me.' })
except Exception, e:
//Es kann immer was Schiefgehen, das sollte man dem pingenden Blog mitteilen
if(e.__class__.__name__ == 'KeyError'):
//insbesondere könnten Parameter fehlen, die man aber erwartet
//da kann man eine schöne Meldung stricken
e = 'Parameter "%s" missing!' % param
return render_to_response('trackback-answer.xml',{'error':'1','errormsg': e })
//Im Zweifel ist halt alles gut. Aber diese Zeile dürfte fast immer unreachable sein.
return render_to_response('trackback-answer.xml',{'error':'0'})
Nun fehlt nur noch der Code für die trackback-answer.xml die man wie üblich zu den anderen Templates ins 'templates'-Verzeichnis legt.
<?xml version="1.0" encoding="utf-8"?>
<response>
{% ifequal error '1' %}
<error>1</error>
<message>{{errormsg}}</message>
{% else %}
<error>0</error>
{% endifequal %}
</response>
Jetzt sollte Django mit Trackbacks von anderen Blogs problemlos umgehen können. Wichtig dafür ist natürlich, dass das andere Blog die entsprechende Trackback-URL des Beitrags kennt. Diese sollte man also dringend mit veröffentlichen.
Auto-Discovery für Trackback-URLs
In den Trackback-Spezifikationen ist aber auch der Fall berücksichtigt, dass das pingende Blog nur die Adresse des verlinkten Beitrags kennt und nicht die genaue Trackback-URL. Das pingende Blog versucht dann die Trackback-URL über eine automatische Erkennung herauszukriegen, indem es die Seite des Beitrags nach einem bestimmten RDF-Metaeintrag durchsucht.
Diese RDF-Metainformationen müssen wie folgt aussehen und man brauch sie eigentlich nur passend ein die Seite/das Template, die den Beitrag anzeigt, einzubauen:
...
<!--
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
<rdf:Description
rdf:about="PERMALINK DES BEITRAGS"
dc:title="TITEL DES BEITRAGS"
dc:identifier="PERMALINK DES BEITRAGS" />
trackback:ping="TRACKBACK-URL DES BEITRAGS"
</rdf:RDF>
-->
...
Danach ist das selbstgebaute Django-Blog bereit für Trackbacks und Autodiscovery.
Trackbacks
Tim Adler schreibt in seinem [Blog](http://www.tim-adler.com/post/trackbacks-mit-django-empfangen/), wie er in Django den Empfang von [Trackbacks](http://de.wikipedia.org/wiki/Trackback) realisiert hat. Seine Implementierung habe ich zum größten Teil über
Comments
Wow, super Post, und so schön schnell.
Damit ich einen "guten" Kommentar schreiben kann, wollte ich doch das eine oder andere ausprobiert haben. (Deshalb der Kommentar so spät, obwohl ich um Post gebeten habe.)
Nun kann ich sagen, dass du den Blog_Name erforderst finde ich vollkommen okay. Werde ich bei mir auch so implementieren.
Leider hast du aber einen Kleinen Fehler: du erlaubst Trackbacks via GET, was gegen die Spec ist. Zumeinen würde ich "REQUEST" durch "POST" ersetzen und zum anderen folgende Zeilen an den Anfang setzen:
<pre>
# Bei dir im Blog
def trackback(request, url_string):
# Only allow POST-requests
if request.method == 'GET':
return HttpResponseRedirect('/post/%s/' % url_string)
# Bei den meisten anderen
def trackback(request, year, month, day, slug):
# Only allow POST-requests
if request.method == 'GET':
return HttpResponseRedirect('/%s/%s/%s/%s/' % (year, month, day, slug))
</pre>
Denke, dass das so viel eleganter ist. So sehen User, die auf Trackback klicken (weil sie z.B. nicht wissen, was es ist) nicht so eine komische Anzeige: "1 Parameter "title" missing!"
Irgendwie hast du recht viel Code in <code>try: ... except: ...</code> geschmissen. Ist nicht ganz PEP-8-konform. Wüsste auf Anhieb aber auch nicht, wie sich das verbessern ließe.
Ansonsten: Daumen hoch. Super Anregungen dabei.
Liebe Grüße aus Köln
Martin
PS.
Wenn der Entry nicht gefunden wurde schickst du nen Fehler raus. Ich glaube Wordpress macht das auch so... Ist es nicht aber geschickter hier ein HTTP404 zu raisen? Sonst ist (aus HTTP-Sicht) der Post nicht da, der Trackback dazu aber...? Ich weiß es nicht, vielleicht hat jemand anders eine Idee?
PPS.
Coole AJAX-Prüfungen beim Kommentar!
Touché! :)
Die Idee mit der POST-Überprüfung ist gut, das werde ich definitiv mal einbauen. Da kann man dann vielleicht auch noch für den User ne Meldung ausgeben, die die Trackback-URL erklärt. Dann hat man auch gleich nen Feature mit dem man Wordpress wieder ein bisschen überlegen ist.
@404 raisen: mmh, stimmt eigentlich. Könnte man vielleicht auch mal ändern. kA was die pingenden Blogs dann machen...aber das ist ja auch deren Problem :)!
Ansonsten danke fürs genaue Lesen, auch wenn ich keine Ahnung habe, was PEP-8 ist. Hauptsache nichts Ansteckendes ;)!
Hallo,
Ne Meldung ist ja ne gute Idee, sowas in der Art:
def trackback(request, url_string):
# Only allow POST-requests
if request.method == 'GET':
request.user.message_set.create(message=_("<a href="http://en.wikipedia.org/wiki/Trackback">Trackbacks</a> are used to connect weblogs."))
return HttpResponseRedirect('/post/%s/' % url_string)
In der base.html könnte man dann, wie in der Doku erklärt (http://www.djangoproject.com/documentation/authentication/#messages), folgendes einbauen:
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
Das sollte alles gewesen sein. (Wichtig noch: man muss ``RequestContext`` an das Template übergeben.)
Super Tip deinerseits mit der Info an den User.
PEP-8? Sage das lieber nicht laut :P
PEP = Python Enhancement Proposals
PEP 8 ist für Pythonentwickler wohl eine der Wichtigsten. (Leute, die Python nutzen, nicht die, die Python entwickeln...): "Style Guide for Python Code"
Finde es klasse, dass es da eine Definition gibt, in PHP hat jeder alles gemacht, und man musste sich immer an neue Stile gewöhnen. In Python sollte das also nicht der Fall sein. (In manchen Punkten würde ich der PEP8 widersprechen, aber wenn das der Standard ist, dann halte ich mich dran. Bin schließlich kein Internet-Explorer-Entwickler ;) )
Liebe Grüße.
Hallo,
das mit der Info schicken, hat mir dann doch keine Ruhe gelassen. Leider geht das ganze nicht über das Message-Feature der Auth-App. Denn dieses Feature gibt es NUR bei *eingeloggten* Nutzern. Nicht eingeloggte bekommen folgenden Fehler:
``'AnonymousUser' object has no attribute 'message_set'``
Hier jetzt die (nicht so schöne und etwas kompliziertere) Alternative:
# settings.py
# 'django.core.context_processors.request' Muss verfügbar sein, wenn man generic views
# nutzt, sonst müsste man nen View schreiben, der die GET-Var messages übergibt
TEMPLATE_CONTEXT_PROCESSORS = ('django.core.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.request')
# Trackback-view
from django.utils.http import urlquote
def trackback(request, url_string):
# Only allow POST-requests
if request.method == 'GET':
message = _('[Trackbacks](http://en.wikipedia.org/wiki/Trackback) are used to connect weblogs. \n'+
'*Use this URL, when you write a new weblog-entry and you want to refer to this entry.*')
return HttpResponseRedirect('/post/%s/?message=%s' % (url_string, urlquote(message)))
# base.html
{% if request.GET.message %}<ul id="messages">
<li>{{ request.GET.message|striptags|markdown }}</li>
</ul>{% endif %}
{% block content %}{% endblock %}
``striptags``-Filter sollte man nutzen, da das ganze ja via GET übergeben wird, weshalb man versuchen sollte das ganze *relativ* sicher zu machen... Sonst können User vielleicht JavaScript einschleusen, etc.
Ist also nicht die schönste Variante, funktioniert aber. Hier ein Beispiel:
http://www.martin-geber.com/weblog/2007/10/29/django-signals-vs-custom-save-method/?message=%5BTrackbacks%5D%28http%3A//en.wikipedia.org/wiki/Trackback%29%20are%20used%20to%20connect%20weblogs.%20%20%0A%2AUse%20this%20URL%2C%20when%20you%20write%20a%20new%20weblog-entry%20and%20you%20want%20to%20refer%20to%20this%20entry.%2A
``/trackback/`` funktioniert soweit, nur habe ich noch Probleme mit dem Akismet, siehe XING-Gruppe: https://www.xing.com/app/forum?op=showarticles&id=6235251&articleid=6235251
Hoffe ich konnte ein wenig helfen.
Liebe Grüße
Martin
PS.
Markdown wäre in den Kommentaren cool:
http://www.djangoproject.com/documentation/templates/#django-contrib-markup
So, nun ist es eingebaut ;)!
Hallo,
wow, das sieht ja mal wirklich klasse aus. *sehr neidisch ist*
Ich habe aber noch eine Anmerkung: Du zeigst deine Seite unter */trackback/ an, was SEO mäßig nicht geschickt ist.
Ich habe einen Redirekt gemacht, nur hatte ich das Problem, dass ich nicht wusste, wie ich diese "Meldung" weiter geben soll. Das Django Message System funktioniert nur mit angemeldeten Nutzern. Also habe ich zunächst die Sache via GET übergeben.
Nun habe ich aber noch etwas besseres gefunden: Sessions.
Ich setze in der Trackback-Funktion folgendes:
request.session['message'] ="Meldung"
Meine Detail-Ansicht musste ich massiv ändern, da es ein Generic View war, ich habe nicht nur diese Session Variable lesen musste (geht via Template relativ einfach), aber ich musste die Message wieder löschen, da sie sonst immer gezeigt worden wäre. Hier nun mein kleiner Vanilla View (ungekürzt):
def entry_detail(request, year, month, day, slug):
message = ""
try:
message = request.session['message']
del request.session['message']
except KeyError: pass
return object_detail(request, queryset=Entry.objects.all(),
date_field='pub_date', year=year, month=month, day=day,
slug=slug, slug_field='slug', month_format='%m',
extra_context={'message': message})
Auf diese Art und Weise wird nur eine Seite geparst und man kann trotzdem alles weiter geben.
Nur noch als letze Anregung.
Liebe Grüße
Martin
PS.
GENIALE ANZEIGE, SIEHT SUPER AUS *wirklich beeindruckt ist*
Tja, da habe ich wohl einmal zu wenig getestet, bevor ich den Code in die Wildnis gelassen habe. Sorry für den doppelten Trackback, wird hoffentlich nicht wieder vorkommen.
Trotzdem vielen Dank für die Anregung mit den Trackbacks.
Gruß, Jens Herrmann.