Alles zum Thema Django & Python

Überschreiben hochgeladener Dateien

Wird eine Datei für ein FileField oder ImageField hochgeladen und exisitiert bereits, überschreibt Django diese Datei nicht sondern hängt einen Tiefstrich an die Datei. Die ältere Datei bleibt ohne Verweis im Dateisystem bestehen.

Überschreibst du also eine Datei rechnung.pdf drei mal, entsteht eine lustige Kaskade im Dateisystem.

  • rechnung.pdf
  • rechnung_.pdf
  • rechnung__.pdf
  • rechnung___.pdf (Der Pfad zu dieser Datei wird im FileField gespeichert)

Es ist nicht immer im Interesse, diese alten Dateien zu behalten. Sei es aus Platzmangel, Datenschutzgründen oder weil man einfach nur Ordnung auf seinem System halten will.

Derzeit gibt es keinen direkten Weg, das überschreiben zu erzwingen. Es gibt schon seit längerer Zeit ein Ticket im Django-Repository aber es schaut nicht so aus, als ob es in Django 1.1 eingespielt wird.

Automatisches Übeschreiben mit einem Custom-Storage ###

Hochgeladene Dateien werden von einem eigenen Modul, dem FileSystemStorage verarbeitet. Es öffnet und speichert die Dateien und prüft, ob der Dateiname schon exisitiert und hängt ggf. einen Tiefstrich an die Datei. Das passiert in der Methode get_available_name. Genau diese ist der Ansatzpunkt um das Überschreiben zu erzwingen.

Hier wird ein eigenes Storage erstellt aber vor dem Speichern der Datei, wird (wenn sie existiert) die alte Datei gelöscht:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.db import models
from django.core.files.storage import FileSystemStorage

class OverwriteFileSystemStorage(FileSystemStorage):
    def get_available_name(self, name):
        if self.exists(name):
            self.delete(name)
        return name

class Invoice(models.Model):
    title = models.CharField(max_length=120)
    invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage())

    def __unicode__(self):
        return self.title

Das Custom-Storage wird dem FileField oder dem ImageField als Attribut storage übergeben. Soll dieses Schema für alle Uploads gelten, ändere in der settings.py den Wert DEFAULT_FILE_STORAGE Standardmässig wird das oben genannte FileSystemStorage verwendet.

1
2
3
4
5
# Original
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'

# Immer alle Dateien überschreiben
DEFAULT_FILE_STORAGE = 'path.to.OverwriteFileSystemStorage'

Den originalen Dateinamen zusätzlich speichern

Dass Dateien nicht grundsätzlich überschrieben werden ist ja grundsätzlich keine schlechte Idee. Schließlich hat man immer ein automatisches Backup aller Änderungen. Lassen wir das Schema so wie es ist aber speichern wir den originalen Dateinamen zusätzlich in einem eigenen Feld. So können wir dem User später den orignalen Dateinamen anzeigen, im Dateisystem bleiben aber alle überschriebenen Dateien erhalten.

Ein upload-Callable ist ein schneller und sicherer Weg, dies zu erreichen. Mehr dazu hatte ich schon in Ordnung im Medienordner geschrieben.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.db import models

class Invoice(models.Model):

    def upload_invoice(instance, filename):
        instance.invoice_orig_filename = filename
        return filename

    title = models.CharField(max_length=120)
    invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage())
    invoice_orig_filename = models.CharField(max_length=100, blank=True)

    def __unicode__(self):
        return self.title

Dateien löschen

Der Vollständigkeit halber: Von Haus aus ist es im Admin noch nicht möglich, hochgeladene Dateien wieder zu löschen. Stephan Jäkel hat ein Admin-Widget konstruiert, dass Dateien auf Klick wieder löscht.

Group By Funktionen in Querysets (Django 1.1+)

In meinem aktuellen Projekt will ich die Anzahl neuer Einträge in einem Model pro Tag in einer Grafik darstellen, ich habe also ein Problem wie:

Gib mir eine Liste aller Tage an denen Beiträge hinzugefügt wurden und wie viele Beiträge waren es an diesem Tag.

Ein SQL-Query dafür ist einfach:

1
SELECT count(id) as counter, created FROM entry_table GROUP BY created

Nun schauen wir mal, wie sich das mit Django 1.1 Bordmitteln lösen lässt. Wir erstellen ein Django-Model:

1
2
3
4
5
from django.db import models

class Entry(models.Model):
    created = models.DateField()
    title = models.CharField(max_length=120)

Und füttern es gleich mit ein paar Testdaten:

1
2
3
>>> Entry.objects.create(created='2009-03-25', title='Irgendein Titel')
>>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel')
>>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel')

Während man in Django 1.0 noch rohes SQL walten lassen musste, wurden Django 1.1 (aktuell noch django-trunk) einige Aggreagations-Funktionen hinzugefügt, um übliche Aufgaben wie Count, Average, Max und Min etc. direkt im Queryset durchführen zu können. Für unseren Fall brauchen wir die Count-Funktion.

1
2
3
4
>>> from django.db.models import Count
>>> Entry.objects.values('created').annotate(counter=Count('id'))
[{'counter': 2, 'created': datetime.date(2009, 3, 24)},
 {'counter': 1, 'created': datetime.date(2009, 3, 25)}]

values('created') triggert hierbei das "GROUP BY created" und Count('id') ist das Pendant zur SQL-Funktion COUNT(id). annotate fügt diese Daten dem SELECT hinzu.

Super einfach, oder? Smiley:  :)

Umgang mit DateTime Feldern

Nun ist es aber in der Regel so, dass man einem Artikel kein DateField sondern ein DateTimeField zuweist, also die Zeit mitspeichert:

1
2
3
class Entry(models.Model):
    created = models.DateTimeField()
    title = models.CharField(max_length=120)

Füttern wir das Model wieder mit ein paar Testdaten:

1
2
3
>>> Entry.objects.create(created='2009-03-25 12:30:00', title='Irgendein Titel')
>>> Entry.objects.create(created='2009-03-24 10:25:00', title='Irgendein Titel')
>>> Entry.objects.create(created='2009-03-24 8:56:00', title='Irgendein Titel')

Und führen unseren obigen Query aus:

1
2
3
4
>>> Entry.objects.values('created').annotate(counter=Count('id'))
[{'counter': 1, 'created': datetime.datetime(2009, 3, 24, 8, 56)},
 {'counter': 1, 'created': datetime.datetime(2009, 3, 24, 10, 25)},
 {'counter': 1, 'created': datetime.datetime(2009, 3, 25, 12, 30)}]

Man sieht, das Ergebnis ist unbrauchbar. Logisch, denn es wird nach Datum und Zeit gruppiert. Wir brauchen also nur das Datum aus dem DateTime-Objekt. In SQL gibt es dafür die Funktion DATE(datetime-Feld) die das Datum extrahiert:

1
SELECT COUNT(id) as counter, DATE(created) as day FROM entry_table GROUP BY day

Dummerweise liefert Django keinen Filter mit, um eine solche Aufgabe zu lösen. Es gibt zwar die dates Methode, um Zeitperioden aus einem Queryset zu extrahieren, diese liefert aber nur die DateTime-Objekte zurück, kein komplettes Queryset, dass für annotate notwendig ist.

Es muss also doch (etwas) rohes SQL her. Dafür ist die extra Methode gedacht. Mit ihr kann man rohes SQL in den Query einsetzen. Folgendes Beispiel resultiert gleich:

1
2
Queryset: Entry.objects.extra(select={'day': 'DATE(`created`)'})
SQL:      SELECT *, DATE(`created`) AS day FROM entry_table

Zusammengefügt schaut unser Queryset jetzt so aus:

1
2
3
4
5
>>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'})
    .values('day').annotate(counter=Count('id'))
>>> entries
[{'counter': 2, 'day': u'2009-03-24'}, 
 {'counter': 1, 'day': u'2009-03-25'}]

Die Tage sind hierbei aber ein unicode-String, ein DateTime-Objekt wäre schöner. Mappen wir den Queryset noch einmal:

1
2
3
4
5
6
>>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'})
    .values('day').annotate(counter=Count('id'))
>>> entries = [{'counter': i['counter'], 'day': datetime.strptime(i['day'], '%Y-%m-%d')} for i in entries]
>>> entries
[{'counter': 2, 'day': datetime.datetime(2009, 3, 24, 0, 0)},
 {'counter': 1, 'day': datetime.datetime(2009, 3, 25, 0, 0)}]

Da sind sie, die Tage an denen Beiträge hinzugefügt wurden inkl. deren Anzahl. Smiley:  :-)

War diesmal vielleicht (noch) verworrener als sonst, wer Fragen dazu hat kann mir wie immer mailen oder in die Kommentare schreiben. Smiley:  :-)

Automatisch lokalisierte Zeitformatierungen 

Ein nur knapp dokumentiertes Feature ist die Lokalisierung von String innerhalb von Templatetags und Filtern. Dort reicht es, den betreffenden String in einen gettext-Shortcut zu setzen:

1
{{ _("hello world")|upper }}

Dieser Token würde als HELLO WORLD ausgegeben und natürlich auch mit den betreffenden .po Dateien lokalisierbar sein. Richtig sinnvoll wird es beim date Filter. Bisher hast du vielleicht das Datum fest vorgegeben:

1
{{ entry.published|date:"d.m.Y H:M:S" }}

Funktioniert im Deutschen ganz gut, aber da ja bekannterweise jede Sprache das Datum und die Zeit irgendwie anders formatiert, ist es eine gute Idee, die Formatierung auch lokalisierbar anzubieten:

1
{{ entry.published|date:_("DATETIME_FORMAT") }}

Schaut ein wenig komisch aus, funktioniert aber. Smiley:  :-) Clevererweise kennt Django bereits den i18n-String DATETIME_FORMAT und liefert für jede mitgelieferte Sprache die entsprechende Formatierung in den gettext-Katalogen mit. Je nach Locale-Einstellung ist die Ausgabe landestypisch:

1
2
3
de-de: 21. März 2009, 20:24
en-us: March 21, 2009, 8:24 p.m.
pt-br: 21 de Março de 2009 às 20:24

Neben DATETIME_FORMAT werden auch gleich DATE_FORMAT, TIME_FORMAT, YEAR_MONTH_FORMAT und MONTH_DAY_FORMAT mitgeliefert. Smiley:  :-)

Sichere Settings in der ServerError Site

Manchmal lässt es sich nicht vermeiden, auf einem Live-System Djangos Fehler (Exception) Seite, den 500-View anzeigen zu lassen. Sie enthält den Traceback sowie den Umgebungsvariablen und allen Settings, Dinge die ein normaler Besucher eigentlich nicht sehen soll.

In den Settings sind schon einige, sicherheitstechnisch riskante Felder gesternt, DB_PASSWORD zum Beispiel. Fast jeder definiert aber eigene Settings, wie ich hier z.B. den Wert TWITTER_USERNAME mit meinem Twitter-Namen.

Dieses Setting ist jetzt zwar nicht hochriskant aber es soll nur als Beispiel dienen. Andere, geheimere Settings wären z.B. dein Flickr-API-Key. Jedenfalls, der zufällige Besucher auf so einer 500er Seite soll diesen Wert niemals sehen.

Eine Möglichkeit ist, den Namen nicht komplett in Großbuchstaben schreiben, twitter_username = 'bartTC' und schon wird er nicht mehr in dieser Liste auftauchen. Das funktioniert zwar wunderbar ... schaut aber komisch aus. Definitionen sollten in Großbuchstaben definiert werden. (Gibts ein PIP dazu?)

Eine bessere Möglichkeit ist es, dem Namen der Funktion eines der Wörter SECRET, PASSWORD oder PROFANITIES_LIST hinzuzufügen.

Ein Wert wie TWITTER_SECRET_USERNAME wird durch das vorhandene SECRET nämlich ausgesternt. Smiley:  :-)

Weiter lesen

Sprachabhängige Template-Imports

Zuerst die Erklärung, der vollständige Code folgt am Ende.

Um Internationalisierung (i18n) bzw. Übersetzung in Django-Templates zu bringen, gibt es verschiedene Möglichkeiten, allen voran die Möglichkeit, Strings mittels trans oder blocktrans zu markieren und in den entsprechenden gettext-Dateien zu übersetzen.

Das funktioniert gut und hat sich bewährt, solange es sich um Strings handelt. Möchte man eine komplette Seite übersetzen, inkl. Bildern und HTML-Code, wird es schwieriger, mal abgesehen davon, dass ich HTML in gettext-Strings einfach häßlich finde.

Eine Möglichkeit ist es, ifequal mit dem aktuellen Sprachcode des Users (LANGUAGE_CODE) zu vergleichen:

1
2
3
4
5
{% ifequal LANGUAGE_CODE "de" %}
    <p>Hier ganz viel deutscher Text.</p>
{% else %}
    <p>Here is a lot of english text, the default language.</p>
{% endif %}

Wird der Inhalt zu lang, kann man die Texte jeweils in eigene Templates auslagern und mittels include importieren:

1
2
3
4
5
{% ifequal LANGUAGE_CODE "de" %}
    {% include "vieltext_deutsch.html" %}
{% else %}
    {% include "vieltext_english.html" %}
{% endif %}

Hier setzt der unten gezeigte Templatetag an. Er arbeitet prinzipiell wie der Standard-Include-Tag aber importiert automatisch das Template mit der jeweiligen Sprache.

Der Templatename muss dabei das Format <templatename>.<languagecode> als Dateinamen besitzen. Angenommen unser User ist mit deutscher Spracheinstellung unterwegs und sein LANGUAGE_CODE wäre de, der Aufruf:

1
{% langinclude "meintext.html" %}

würde hier zuerst das Template meintext.html.de suchen und importierten. Schlägt dies fehl, importiert es wie gewohnt das Template meintext.html.

Hier nun der Templatetag:

 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
47
48
49
50
51
52
53
54
from django.template import Library, Node
from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable
from django.template.loader_tags import IncludeNode
from django.template.loader import get_template
from django.conf import settings

register = Library()

class ConstantLanguageIncludeNode(Node):
    def __init__(self, template_path):
        self.template_path = template_path

    def render(self, context):
        try: 
            t = get_template('%s.%s' % (self.template_path, context['LANGUAGE_CODE']))
        except TemplateDoesNotExist, KeyError:
            t = get_template(self.template_path)
        except:
            if settings.TEMPLATE_DEBUG:
                raise
            return ''
        return t.render(context)

def do_language_include(parser, token):
    """
    Looks up for a template based on the template-name plus the current users language code.
    Loads the template and renders it with the current context.

    Example::

        {% langinclude "foo/some_include.html" %}

    Based on the users LANGUAGE_CODE, assumed we have 'de', it tries to render the
    template 'foo/some_include.html.de'. If that doesn't exists, it renders the 
    template 'foo/some_include.html'. This is the default behavior of the include-Tag.

    Basically this is a shortcut for the following code, just with a fallback for the
    default template::

        {% ifequal LANGUAGE_CODE "de" %}
            {% include "foo/some_include.html.de" %}
        {% else %}
            {% include "foo/some_include.html" %}
        {% endifequal %}
    """
    bits = token.contents.split()
    if len(bits) != 2:
        raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0]
    path = bits[1]
    if path[0] in ('"', "'") and path[-1] == path[0]:
        return ConstantLanguageIncludeNode(path[1:-1])
    return IncludeNode(bits[1])

register.tag('langinclude', do_language_include)

Der Quellcode auf djangosnippets.org.

Ordnung im Medienordner: Dynamische Upload-Pfade

Djangos FileField und das davon abgeleitete ImageField Feld enthalten ein upload_to-Argument das angibt, wohin die Datei im statischen Medienordner gespeichert werden soll:

1
2
3
4
5
6
7
8
class Image(models.Model):
    image = models.ImageField(upload_to='images/')

    def __unicode__(self):
        return self.image.name

    def get_absolute_url(self):
        return self.image.url

Ein hochgeladenes Bild wäre dann über die URL /media/images/filename.jpg zu erreichen. /media/ ist der URL-Pfad zu den statischen Medien, der zuvor in der settings.py mit MEDIA_URL definiert wurde.

upload_to muss aber kein festgelegter Pfad sein, es kann auch ein callable, also eine Funktion oder jede Klasse die ein __call__ Objekt bietet, verwerten.

Wozu braucht man das? Immer dann, wenn der Pfad dynamisch sein soll. Im folgenden Beispiel soll eine hochgeladene Datei in mehreren Unterordnern abgelegt werden, wobei die Buchstaben der Unterordner die Anfangsbuchstaben der Datei sind, Huh, kompliziert? Gar nicht:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def bilder_in_unterordnern(instance, filename):
    if len(filename.rsplit('.')[0]) >= 3: # Dateiname ohne Extension muss
                                          # mindestens 3 Zeichen besitzen
        filename = "images/%s/%s/%s/%s" % (filename[0], filename[1],
                                           filename[2], filename)
    return filename

class Image(models.Model):
    image = models.ImageField(upload_to=bilder_in_unterordnern)

# ...

Das upload_to Argument enthält den Namen einer Funktion, die den neuen Dateinamen (inklusive Pfad) zurück gibt. Beachte aber, dass das Funktionsargument hier keine Klammern besitzt, sonst würde sie nur einmal, beim Starten des Webservers, ausgeführt werden.

Diese callable-Funktion besitzt immer zwei Argumente, die aktuelle Instanz des Modelobjekts und den originalen Dateinamen. Der Rückgabewert ist der neue Dateiname.

Bilder die mit dem obigen Schema hochgeladen werden, würden dabei diese Pfade erhalten:

  • bild.jpg > /media/images/b/i/l/bild.jpg
  • foobar.jpg > /media/images/f/o/o/foobar.jpg
  • ...

Schon ganz hübsch. Smiley:  :) Richtig sinnvoll wird es aber erst, wenn der Bildpfad Werte aus der selben oder gar einer anderen Model-Instanz enthalten soll.

Angenommen die Bilder sind mit einem Artikel (von einem Weblog, natürlich) verknüpft und sollen als Dateipfad den Slug des verknüpften Artikels enthalten, so kann das Callable auf die aktuelle Model-Instanz und dessen Verknüpfungen zugreifen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def bild_mit_articleslug(instance, filename):
    return 'images/%s/%s' % (instance.article.slug, filename)

class Article(models.Model):
    title = models.CharField(max_length=100, blank=False)
    slug = models.SlugField()
    content = models.TextField()

    def __unicode__(self):
        return self.title

class Image(models.Model):
    article = models.ForeignKey(Article)
    image = models.ImageField(upload_to=bild_mit_articleslug)

Angenommen unser Artikel hat den Titel "Mein neuer Blogbeitrag" so wären die Dateinamen so:

  • bild.jpg > /media/images/mein-neuer-blogbeitrag/bild.jpg
  • foobar.jpg > /media/images/mein-neuer-blogbeitrag/foobar.jpg
  • ...

So schafft man Ordnung im Dateisystem. Smiley:  :)

Ausnahme Primärschlüssel

Eine Ausnahme ist, wenn der Dateiname des Bildes den Primärschlüssel des Objekts enthalten soll. Da, bei einem Insert, der Primärschlüssel noch nicht gesetzt ist, existiert dieses Attribut noch nicht. Ein Umweg ist, erst das Bild hochzuladen, das Objekt zu speichern dann das Bild entsprechend umzubenennen und das Objekt wiederum zu speichern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Image(models.Model):
    image = models.ImageField(upload_to='tmp/')

    def save(self, *args, **kwargs):
        # Modelobjekt speichern und einen Primärschlüssel (self.pk) erhalten
        super(Image, self).save(*args, **kwargs)

        # Nur umbennenen wenn sich die Datei noch im tmp-Pfad befindet
        MEDIA_ROOT = os.path.abspath(settings.MEDIA_ROOT)
        if self.image.path.startswith(os.path.join(MEDIA_ROOT, 'tmp/')):

            # Neuen Dateinamen mit Pfad und Primärschlüssel generieren
            new_filename = '%s%s%s' % (os.path.join(MEDIA_ROOT, 'images/'),
                self.pk, os.path.splitext(self.image.name)[1])

            # Letztendlich die Datei umbennenen und den neuen Pfad speichern
            old_filename = os.path.join(MEDIA_ROOT, self.image.name)
            os.rename(old_filename, new_filename)

            # Den neuen Dateinamen im Model setzen und noch einmal speichern
            self.image = new_filename
            super(Image, self).save(*args, **kwargs)

Das ganze geht bestimmt noch hübscher und enthält einige Fallstricke, so muss der Ordner images/ schon existieren. Aber seis drum, Bilder die so hochgeladen werden, würden als Dateinamen den Primärschlüssel des jeweiligen Modelobjekts erhalten:

  • bild.jpg > /media/images/1.jpg
  • foobar.jpg > /media/images/2.jpg
  • ...

Dateinamen mit Datum und Zeit

Der Vollständigkeit halber sei noch erwähnt, dass das upload_to Argument auch ohne Umwege strftime-Argumente verwerten kann. Ein upload_to='/images/%Y/%m/' würde das Bild im Ordner /images/2009/02/bild.jpg speichern.

Spass mit Newforms-Admin - Ganz schnell Widgets zuweisen 

In den vergangenen Spass mit Newforms-Admin Beiträgen hab ich immer wieder gezeigt, wie man das Widget (die HTML-Ausgabe) eines Feldes beeinflusst und ändert.

Als Beispiel nochmal ein simples Model, bei dem letztendlich der Titel in großer Schrift ausgegeben werden soll.

1
2
3
4
5
from django.db import models

class Foobar(models.Model):
    title = models.CharField(max_length=100)
    text = models.TextField(blank=True)

Also basteln wir uns – wie gehabt – eine ModelAdmin-Klasse und darin eine Methode formfield_for_dbfield. Diese durchläuft alle Felder und wenn sie auf den passenden Titel trifft, ändert sie das Widget dazu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.contrib import admin
from django import forms
from django.utils.safestring import mark_safe
from .models import Foobar

class BigTitleWidget(forms.widgets.Input):
    def render(self, name, value, attrs=None):
        field = '<input name="%s" value="%s" style="font-size:1.5em"/>' % (name, value)
        return mark_safe(field)

class FoobarAdmin(admin.ModelAdmin):
    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(FoobarAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == "title":
            field.widget = BigTitleWidget()
        return field

admin.site.register(Foobar, FoobarAdmin)

Aber ist das nicht super hässlich? Irgendwann bin ich mal über einen Codeblock von Jannis gestolpert und hab gesehen, dass die ModelAdmin-Klasse ein Attribut form besitzt, dass das automatisch generierte ModelForm mit einem eigenen überschreiben kann. Hey, und das ist sogar dokumentiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.contrib import admin
from django import forms
from django.utils.safestring import mark_safe
from .models import Foobar

class BigTitleWidget(forms.widgets.Input):
    def render(self, name, value, attrs=None):
        field = '<input name="%s" value="%s" style="font-size:1.5em"/>' % (name, value)
        return mark_safe(field)

class FoobarForm(forms.ModelForm):
    title = forms.CharField(widget=BigTitleWidget())

class FoobarAdmin(admin.ModelAdmin):
    form = FoobarForm

admin.site.register(Foobar, FoobarAdmin)

Das ist doch viel schöner! Smiley:  :-)

Also nie wieder formfield_for_dbfield? Nicht ganz, denn innerhalb des ModelForms hat man keinen Zugriff auf die anderen Parameter des aktuellen Admin-Views. Will man dem Widget weitere Parameter übergeben (wie z.B. die aktuelle User-ID, hier erklärt, führt der Weg weiterhin über formfield_for_dbfield.

Unter der Lupe: Template-Context-Prozessoren

Djangos Templates stehen zwei unterschiedliche Arten zur Verfügung, wie Variablen ins Template gelangen (Template-Filter und -Tags jetzt außen vorgelassen). Die lokalen Variablen, die pro View festgelegt werden und die Template-Context-Prozessoren (im folgenden TCP genannt), jeweils ein Dictionary mit Variablen, das allen Templates zur Verfügung steht.

Einige TCPs sind in der Grundkonfiguration eines Django-Projekts schon mit dabei und bestimmt hast du sie schon einmal gesehen:

1
2
3
4
5
TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
)

Hinweis: Wenn du ein neues Projekt mittels django-admin.py startproject foo erstellt hast, ist dieser Eintrag nicht in der settings.py vorhanden. Er steht aber in den Grundeinstellungen django.conf.global_settings. Wenn du etwas ändern willst, packe den oberen Schnipsel ans Ende deiner settings.py.

Diese Tupel wird in der Einstellung settings.TEMPLATE_CONTEXT_PROCESSORS festgelegt. Am bekanntesten wird wohl der media-TCP sein, er stellt allen Templates die Variable MEDIA_URL zur Verfügung, ein Link zum Ordner mit den statischen Mediendateien.

TCPs können natürlich auch selbst erstellt werden, die Syntax ist sehr einfach:

1
2
3
4
def mein_prozessor(request):
    return {
        'VARIABLEN_NAME': 'wert',
    }

Es ist eine einfache Funktion die als einziges Argument die aktuelle request-Variable nimmt und ein Dictionary zurückgibt. Eingebunden wird sie in die settings.py wie oben schon beschrieben:

1
2
3
4
5
6
TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
    'mein_app.context_processors.mein_prozessor',
)

Es ist eine gute Idee, die Datei die diese Funktion beinhaltet, auch 'context_processors.py' zu nennen.

Nochmal im Vergleich

Da sich mit Quellcode eine Sache am besten verstehen lässt, hier noch einmal ein einfacher View:

1
2
3
4
5
6
7
def startseite(request):
    return render_to_response(
        'pfad/zum/template.html',
        {'headline': 'Meine Startseite',
         'text': 'Lorem ipsum dolor bla.'},
        RequestContext(request)
    )

Die hier benutzte render_to_response Funktion gibt das Template (erstes Argument) zurück mit den lokalen Variablen (zweites Argument) und stellt auch alle globalen TCPs bereit (drittes Argument). In einer flauschigen Grafik schaut das so aus:

Was nutzt man wann?

Wann man TCPs nutzt dürfte nun klar sein. Soll ein Wert jedem Template zur Verfügung stehen, baut man dafür einen TCP. Ein Beispiel ist auf dieser Seite meine Fußleiste, dort sind auf jeder Seite die neuesten Blogbeiträge und Bookmarks zu sehen.

Der FAQ-Teil

Nun bist du vielleicht auf diese Seite gelangt, weil man dich im Django-Channel hierher verwiesen hat. Jetzt kommen die häufigsten Fehler im Umgang mit Template-Context-Prozessoren (TCP).

Eine typisches Problem dürfte zum Beispiel sein:

Ich kann in meinem Template die MEDIA_URL Variable nicht nutzen, sie gibt nur einen leeren String zurück.

Mögliche Fehlerquellen

1. Groß/Kleinschreibung

Ganz simpel eigentlich. In Django-Templates werden Variablen zwischen Groß- und Kleinschreibung unterschieden. Media_URL ist also nicht das selbe wie MEDIA_URL.

2. Du hast einen eigenen View geschrieben der render_to_response nutzt

Der häufigste Fehler ist, dass du render_to_response vergessen hast, als drittes Argument den Request-Context zur Verfügung zu stellen. Schau nochmal auf die Beispielfunktion von oben:

1
2
3
4
5
6
7
8
from django.template.context import RequestContext
def startseite(request):
    return render_to_response(
        'pfad/zum/template.html',
        {'headline': 'Meine Startseite',
         'text': 'Lorem ipsum dolor bla.'},
        RequestContext(request)
    )

RequestContext analysiert im Prinzip die Module aus settings.TEMPLATE_CONTEXT_PROCESSORS und stellt sie dem Template zur Verfügung.

Djangos Generic-Views machen das automatisch! Warum render_to_repsonse nicht? Im Prinzip geht es darum, dass du die Möglichkeit hast, diese Variablen nicht dem Template zur Verfügung zu stellen. Klingt komisch, ist aber so. Oft ist dies der Fall, wenn Dritte Zugriff auf die Templates haben und nicht alle globalen Variablen wissen sollen.

Das ist aber nicht DRY *kreisch*. Es gibt Mittel und Wege das semi-automatisch zu machen. Auf dieser Seite sind einige Shortcuts erklärt.

3. Du willst auf den request TCP zugreifen

Bis vor Django 1.0 war der TCP django.core.context_processors.request noch Teil der Grundeinstellungen. Dieser wurde nun (aus Sicherheits- oder Performancegründen, ich weiß es nicht) entfernt. Viele 3rd-Party-Apps nutzen diesen TCP ohne zu dokumentieren, wie er eingebunden wird.

Wenn du in deiner settings.py noch keine Definition für TEMPLATE_CONTEXT_PROCESSORS hast, packe diesen Schnipsel ans Ender der Datei:

1
2
3
4
5
6
TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
    'django.core.context_processors.request',
)

Alternativ, wenn du schon eine Prozessoren-Liste hast, packe einfach django.core.context_processors.request dazu.

Das wars, eine erschöpfende Antwort auf die #1 FAQ im Django IRC-Channel. Smiley:  :-)

Request-unabhängige Locale-Einstellungen 

Manchmal ist es nötig, in einem Request/View verschiedene Locale-Einstellungen zu nutzen. Der häufigste Fall dürfte der Versand von E-Mails an verschiedene User sein, die jeweils die Nachricht in ihrer Sprache erhalten sollen.

Man könnte einfacherweise für diese E-Mail für jede Sprache ein eigenes Template anlegen, aber wozu gibt es die gettext-Unterstütung. Smiley:  :)

Djangos I18N-Kern bringt eine Funktion django.utils.translation.activate mit, mit der man zur Laufzeit die Locale-Einstellung ändern kann.

Als Beispiel dient ein einfaches Template mit einem Translation-String.

1
2
3
4
{% spaceless %}
    {% load i18n %}
    {% trans "Thanks for using our site!" %}
{% endspaceless %}

Und eine Dummy-Funktion. Dabei wird über die Sprachen iteriert (normalerweise würde die jeweilige Spracheinstellung im Userprofil gespeichert sein) und das email.txt Template mit der jeweiligen Sprache im Terminal ausgegeben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from django.template.loader import render_to_string
from django.utils.translation import get_language, activate

current_language = get_language()
languages_to_render = ('en-us', 'de-de', 'pt-br', 'fr')

for lang in languages_to_render:
    activate(lang)
    print render_to_string('email.txt')

activate(current_language)

Ausgabe:

1
2
3
4
Thanks for using our site!
Vielen Dank, dass Sie unsere Seite benutzen!
Obrigado por usar nosso site!
Merci d'utiliser notre site !

Einfach oder? Beachten sollte man noch, dass man den originalen Locale-Wert vor dem ändern speichert (django.utils.translation.get_language liefert den aktuellen Wert) und am Ende wieder aktiviert. Sonst würde in diesem Fall der Rest des Views mit der französischen Locale weiterlaufen.

Eine bessere settings.py

Bei einem frischen Django-Projekt ist so ziemlich die erste Änderung bei mir die settings.py. Es gibt einige Sachen, die fehlen in der Grundkonfiguration immer. So z.B. der request-Context-Prozessor, Caching-Einstellungen oder die Mailserver-Einstellungen (ich betreibe lokal keinen SMTP). Daneben natürlich noch die Sachen, die ich in Wiederverwendbare Django-Projekte gepostet habe.

Irgendwann habe ich mir mal eine generische settings.py erstellt in der ich auf Anhieb alle, für mich nötigen, Einstellungen vorfinde und diese mit sinnvollen Werten vorgeingestellt. Bei jedem neuen Projekt überschreibe ich die settings.py zuerst einmal mit dieser Variante. Vielleicht findet es ja jemand interessant. Smiley:  :-)

  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
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import os

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
PROJECT_NAME = os.path.split(PROJECT_ROOT)[-1]

# ==============================================================================
# debug settings
# ==============================================================================

DEBUG = True
TEMPLATE_DEBUG = DEBUG
INTERNAL_IPS = ()
if DEBUG:
    TEMPLATE_STRING_IF_INVALID = 'STRING_NOT_SET'

# ==============================================================================
# cache settings
# ==============================================================================

CACHE_BACKEND = 'locmem://'
CACHE_MIDDLEWARE_KEY_PREFIX = '%s_' % PROJECT_NAME
CACHE_MIDDLEWARE_SECONDS = 600

# ==============================================================================
# email and error-notify settings
# ==============================================================================

ADMINS = (
    # ('Your Name', 'your_email@example.com'),
)

MANAGERS = ADMINS

DEFAULT_FROM_EMAIL = 'from-mail@example.com'
SERVER_EMAIL = 'error-notify@example.com'

EMAIL_SUBJECT_PREFIX = '[%s] ' % PROJECT_NAME
EMAIL_HOST = 'localhost'
EMAIL_PORT = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = False

# ==============================================================================
# auth settings
# ==============================================================================

LOGIN_URL = '/accounts/login/'
LOGOUT_URL = '/accounts/logout/'
LOGIN_REDIRECT_URL = '/'

# ==============================================================================
# database settings
# ==============================================================================

DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = os.path.join(PROJECT_ROOT, 'dev.db')
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

# ==============================================================================
# i18n and url settings
# ==============================================================================

TIME_ZONE = 'Europe/Berlin'
LANGUAGE_CODE = 'de'
LANGUAGES = (('en', 'English'),
             ('de', 'German'))
USE_I18N = True

SITE_ID = 1

MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'site_media')
MEDIA_URL = '/media/'
ADMIN_MEDIA_PREFIX = '/django_admin_media/'

ROOT_URLCONF = '%s.urls' % PROJECT_NAME

# ==============================================================================
# application and middleware settings
# ==============================================================================

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'django.contrib.humanize',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.http.ConditionalGetMiddleware',
#    'django.middleware.gzip.GZipMiddleware',
    'django.middleware.common.CommonMiddleware',
)

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',
)

TEMPLATE_DIRS = (
    os.path.join(PROJECT_ROOT, 'templates'),
)

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
#    'django.template.loaders.eggs.load_template_source',
)

# ==============================================================================
# the secret key
# ==============================================================================

try:
    SECRET_KEY
except NameError:
    SECRET_FILE = os.path.join(PROJECT_ROOT, 'secret.txt')
    try:
        SECRET_KEY = open(SECRET_FILE).read().strip()
    except IOError:
        try:
            from random import choice
            SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)])
            secret = file(SECRET_FILE, 'w')
            secret.write(SECRET_KEY)
            secret.close()
        except IOError:
            Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE)

# ==============================================================================
# third party
# ==============================================================================

# ..third party app settings here

# ==============================================================================
# host specific settings
# ==============================================================================

try:
    from local_settings import *
except ImportError:
    pass

Der Mythos vom statischen Servieren

In Djangos Dokumentation wird empfohlen, statische Dateien durch den Webserver ausliefern zu lassen. Was bedeutet das? Django läuft doch auf dem Webserver.

Richtig

Im Prinzip geht es darum, Dateien die Django nicht bearbeiten muss, nicht durch den Django-Prozess zu schleifen. Statische Dateien sollen also den direkten Weg von der Festplatte zum Webserver finden (grüne Linie).

Das – ich nenne es mal – Problem ist, dass Django eine Funktion bereit stellt, statische Dateien durch Django zu schleifen: How to serve static files.

Falsch

Hierbei wandern alle statischen Dateien durch den Django-Prozess anstatt dass sie direkt an den Webserver geliefert werden. Das ist ganz klar ineffizient.

Warum gibt's das dann? In erster Linie ist es für Entwickler die den Devserver nutzen gedacht und während der Entwicklung keinen zusätzlichen Webserver zum Static-Serving aufsetzen möchten.

Fazit also: Vermeide Static-Serving durch Django um jeden Preis – es gibt natürlich Ausnahmen, siehe weiter unten.

PHP macht das doch auch so

Letzens hatte mich ein PHP-Entwickerl auch gefragt, wieso er da nicht Django nehmen kann, dass geht doch auch mit PHP.

Oberes ist ein Zitat aus dem #django-de Channel. PHP macht es aber (unbewusst) genauso wie Django. Statische Dateien werden auch direkt vom Webserver geliefert und nicht erst durch den PHP-Prozessor geschleift. Dieser verarbeitet im Normalfall nur Dateien mit der Endung .php.

Ein Static-Serving durch PHP würde etwa so aussehen, dass der Webserver ein Stylesheet mit 'GET /stylesheet.php' anfordert, und diese Datei das unten gezeigte Script ausführt.

1
<?php echo file_get_contents('/var/www/static/stylesheet.css'); ?>

Ziemlich dämlich, oder? Aber leider alles schon gesehen. Smiley:  :-(

Spezialisten

Überlasse das Static-Serving einem Server der besser dafür geeignet ist, zum Beispiel nginx oder Lighty.

Den oberen Satz hört man sehr häufig wenn es es Static-Serving geht. Was ist gemeint?

Zuerst einmal, nginx und lighttpd, oder auch liebevoll lighty genannt, sind Webserver, die auf das Ausliefern statischer Dateien optimiert sind. Dabei sind sie nicht unbedingt schneller als der Apache, ihre Stärke liegt in der Auslieferung von mehreren tausend Dateien pro Sekunde, denn dabei sind sie weniger RAM- und CPU-hungrig und skalieren besser.

Es gibt einen Weg, das beste beiden Webservern (Apache für Django, nginx für statische Dateien) herauszuholen. Dabei übernimmt nginx (oder lighty) als Frontend-Server alle Anfragen des Webbrowsers und serviert die statischen Dateien direkt. Anfragen auf dynamische Dateien werden an den Apache-Prozess weitergeleitet, der auf einem anderen Port (also nicht :80) lauscht. (Konfigurationsbeispiel)

Sieht cool aus

Das Ganze ist dann schon ein recht kompliziertes Setup. Meine persönliche Meinung ist, wenn man den Punkt erreicht hat, dass ein spezialisierter Webserver für statische Dateien nötig ist, sollte man den statischen Kram auf eine eigene Maschine auslagern:

Made for suits

Letztendlich ist alles eine Frage des Budgets. Nicht unerwähnt soll bleiben, dass es auch Firmen gibt, die sich auf das Static-Serving spezialisiert haben. Akamai ist ein Vertreter aber auch Amazons S3 Service ist hervorragend dafür geeignet.

Was wäre wenn?

Es gibt natürlich auch das Problem, dass man statische Dateien durch Django schleifen muss. Beispiele:

  1. Die Datei soll nur für bestimmte, authentifizierte User verfügbar gemacht werden.

    Django stellt für solche Fälle eine Möglichkeit zur Verfügung, dass sich der Apache an Djangos Auth-Backend koppeln kann. Mehr Infos dazu in der Dokumentation.

  2. Die Datei wird dynamisch erzeugt, zum Beispiel ein PDF-Export der aktuellen Seite.

    Vielleicht wäre hier eine Lösung, die Datei im Medienordner zu speichern und einen Location-Header zu dieser Datei zu senden. Dann übernimmt der Webserver wieder das Ausliefern der statischen Dateien und man erhält zudem ein statisches Caching der Dateien.

Jedenfalls, wenn es denn nicht anders möglich ist versuche große Dateien als Iterator an das Response-Objekt zu übergeben. Damit wird nicht die gesamte Datei in den Prozess geladen (bei einer 80MB Datei wäre der wsgi-Prozess dann 90MB+ groß) sondern in kleinen Häppchen (Chunks) durchgeschleift.

Beachte dazu aber, dass die GZip-Middleware diesen Prozess zerstören kann: Ticket #7581. Aber auch dafür gibt es eine einfache, performante Lösung. Smiley:  :-)

Noch ein wichtiger Nachtrag:

Auch wenn in den Bildern mod_python angegeben ist, empfehle ich euch doch ganz stark mod_wsgi als Kleber zwischen Django und Apache. Es ist resoucen schonender, performanter und allgemein sympathischer. Leider aber hat mod_wsgi kein Logo das ich benutzen konnte. Smiley:  ;-)

Mehr dazu

Mehr Lesestoff und vor allem Konfigurations-Beispiele für verschiedene Webserver-Szenarien findest du im deutschsprachigen django-hosting Wiki.

Forms: Dynamisch Felder hinzufügen

Kurzer und schmerzloser Tipp für zwischendurch: Wie kann man in einem NewForms-Formular dynamisch Felder hinzufügen?

Abstraktion

Am einfachsten per Abstraktion. Das Formular mit den weiteren Feldern leitet sich einfach vom Basisformular ab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django import forms

class Userform(forms.Form):
    username = forms.CharField()
    passwort = forms.CharField(widget=forms.PasswordInput(render_value=False))

class UserformKontaktdaten(Userform):
    #                      ^ Abgeleitet von Userform
    vorname = forms.CharField()
    nachname = forms.CharField()

Ausgabe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> f = Userform()
>>> print f.as_ul()
<li><label for="id_username">Username:</label> <input type="text" name="username" id="id_username" /></li>
<li><label for="id_passwort">Passwort:</label> <input type="password" name="passwort" id="id_passwort" /></li>

>>> f = UserformKontaktdaten()
>>> print f.as_ul()
<li><label for="id_username">Username:</label> <input type="text" name="username" id="id_username" /></li>
<li><label for="id_passwort">Passwort:</label> <input type="password" name="passwort" id="id_passwort" /></li>
<li><label for="id_vorname">Vorname:</label> <input type="text" name="vorname" id="id_vorname" /></li>
<li><label for="id_nachname">Nachname:</label> <input type="text" name="nachname" id="id_nachname" /></li>

Laufzeitabhängig

Möchte man die Felder aber schon während der Formulargenerierung dynamisch hinzufügen, überschreibt man einfacherweise den Konstruktor und legt die fügt die neuen Felder zu den bestehenden hinzu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Userform(forms.Form):
    username = forms.CharField()
    passwort = forms.CharField(widget=forms.PasswordInput(render_value=False))

    def __init__(self, kontaktdaten=False, *args, **kwargs):
        super(Userform, self).__init__(*args, **kwargs)

        # Hier dynamisch Felder hinzufuegen
        if kontaktdaten:
            self.fields['vorname'] = forms.CharField()
            self.fields['nachname'] = forms.CharField()

Ausgabe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> f = Userform()
>>> print f.as_ul()
<li><label for="id_username">Username:</label> <input type="text" name="username" id="id_username" /></li>
<li><label for="id_passwort">Passwort:</label> <input type="password" name="passwort" id="id_passwort" /></li>

>>> f = Userform(kontaktdaten=True)
>>> print f.as_ul()
<li><label for="id_username">Username:</label> <input type="text" name="username" id="id_username" /></li>
<li><label for="id_passwort">Passwort:</label> <input type="password" name="passwort" id="id_passwort" /></li>
<li><label for="id_vorname">Vorname:</label> <input type="text" name="vorname" id="id_vorname" /></li>
<li><label for="id_nachname">Nachname:</label> <input type="text" name="nachname" id="id_nachname" /></li>

Spass mit Newforms-Admin - Read-Only Felder

In Automatische Felder zeigte ich letztens, wie man Felder automatisch mit einem Wert versieht. Nun geht es wieder einen Schritt weiter: Wie kann man Felder als "Text" anzeigen, so dass keine Eingabe des Users möglich ist.

Einfach und schnell

Das Stichwort ist: Widgets. Widgets legen fest, wie die Daten später im HTML ausgegeben werden – und wie Widgets den Admin-Felder zugewiesen werden, habe ich in den letzten Artikeln schon zur Genüge erklärt.

HTML-Formular-Elemente besitzen schon seit gefühlten 200 Jahren ein Attribut, dass sie "nur lesbar" macht: disabled, oder XHTML-Konform: disabled="disabled". Attribute lassen sich blitzschnell den Feldern, besser gesagt den Widgets, zuweisen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.contrib import admin
from myproject.weblog.models import Entry

class EntryAdmin(admin.ModelAdmin):

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

        # Das Titel-Feld read-only setzen
        if db_field.name == "title":
            field.widget.attrs = {'disabled': 'disabled'}
        return field

Read-Only Felder

Und fertig ist die Geschichte. Das ist mal Rapid Development. Leider – wie immer – nicht ganz, es gibt noch einen Stolperstein: Auch beim Anlegen eines neuen Eintrags wird das Feld ausgegraut, die disabled-Geschichte darf also nur Änderungen (change) eines Artikels betreffen.

Read-Only Felder

Der Methode formfield_for_dbfield fehlt ein "Status-Flag", ob wir uns im Add- oder Change-Modus befinden. Das ist aber schnell nachgeholt, die Methode change_view markiert zur Laufzeit das Admin-Objekt einfach als "_is_change" und während der Widget-Manipulation wird darauf geprüft.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class EntryAdmin(admin.ModelAdmin):

    def add_view(self, request, *args, **kwargs): 
        self._is_change_mode = False
        return super(EntryAdmin, self).add_view(request, *args, **kwargs)

    def change_view(self, request, object_id, *args, **kwargs): 
        self._is_change_mode = True
        return super(EntryAdmin, self).change_view(request, object_id, *args, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

        # Das Titel-Feld read-only setzen
        # aber nur wenn wir uns im change-Modus befinden
        if db_field.name == "title" and self._is_change_mode:
            field.widget.attrs = {'disabled': 'disabled'}
        return field

Jetzt aber richtig

Ok, Problem gelöst aber schön ist was anderes. Hier im Firefox wird der Text grau auf grau dargestellt. Die disabled-Sache kann also nicht der heilige Gral sein. Ein anderer Ansatz ist: Zeige den Text/Inhalt des Feldes an, ohne Formularelemente.

Auch hier ist es eine Widgetsache. Wir brauchen also ein Widget, dass statt eines HTMl-Formular-Elements einfach nur den Inhalt ausspuckt. Eigentlich ganz einfach:

 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
from django.contrib import admin
from django import forms
from django.utils.safestring import mark_safe
from myproject.weblog.models import Entry

class ReadOnlyWidget(forms.HiddenInput):

    def __init__(self, append_text, *args, **kwargs):
        self.append_text = append_text
        super(ReadOnlyWidget, self).__init__()

    def render(self, *args, **kwargs):
        field_value = super(ReadOnlyWidget, self).render(*args, **kwargs)
        return mark_safe("%s <strong>%s</strong>" % (field_value, self.append_text))

class EntryAdmin(admin.ModelAdmin):

    def add_view(self, request, *args, **kwargs): 
        self._is_change_mode = False
        return super(EntryAdmin, self).add_view(request, *args, **kwargs)

    def change_view(self, request, object_id, *args, **kwargs): 
        self._is_change_mode = True
        self._obj = Entry.objects.get(pk=object_id)
        return super(EntryAdmin, self).change_view(request, object_id, *args, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

        # Dem Titel-Feld das read-only Widget zuweisen
        # aber nur wenn wir uns im change-Modus befinden
        if db_field.name == "title" and self._is_change_mode:
            field.widget = ReadOnlyWidget(append_text=self._obj.title)
        return field

Read-Only Felder

Was passiert da? Das Titel-Feld, ein Input-Textfeld, wird in ein HiddenInput umgewandelt, im Quellcode ist es also als <input type="hidden" .../> zu sehen. Der eigentlich sichtbare Text wird aus dem aktuellen Model-Objekt gezogen und einfach drangehängt. Wiedermal hat formfield_for_dbfield keinen Zugriff auf das aktuell zu ändernde Objekt, aber wie ihr seht, kann man es einfach in der change_view Methode auslesen und der Klasse global zuweisen.

Stolperstein ist hier das Hidden-Input-Feld. Ein User mit – ich nenne es mal: genügend krimineller Energie – kann mehr oder weniger einfach den Inhalt des Hidden-Feldes manipulieren. Vielleicht währe es sinnvoll, den Wert des Hidden-Feldes zu verschlüsseln oder mit einer Prüfsumme (Hash) zu markieren. Andererseits, ein User der es drauf anlegt, mit allen Mitteln so einen Titel zu ändern, sollte sowieso keinen Zugang zur Admin-Ebene bekommen. Smiley:  ;-)

Andere Wege

Bei dieser Sache beschleicht mich mehr und mehr das Gefühl, dass der Ansatz einfach zu kompliziert ist. Dieses "formfield_for_dbfield hat keinen direkten Zugriff auf request, model-objekt, change_status, also holen wir das alles in den globalen Scope" nervt.

Aber es gibt auch andere Ansätze, am besten gefällt mir derzeit die Methode auf djangosnippets.org. Und wer ein wenig googelt findet noch hundert weitere Möglichkeiten, Admin-Felder read-only zu setzen.

Seis drum, letztendlich geht es mir bei dieser Artikelserie auch nicht darum, den perfekten Lösungsvorschlag für Problem X zu zeigen, sondern viel mehr Mittel und Wege zu zeigen, wie man ein solches Problem lösen könnte. Am Ende kommen noch zehn weitere Problemchen dazu (Stichwort: Permission-Handling) und jeder muss für sich selbst wissen, was der beste Lösungsweg für sein Problem ist. Amen. Smiley:  :-)

Multiple Aktionen in Djangos Administration - Reloaded

Vor mehr als einem halben Jahr hatte ich einen Blogpost zum Thema Multiple Aktionen in Djangos Administration geschrieben. Nun ist schon einige Zeit ins Land gegangen, Django besitzt jetzt den Status Eins Punkt Null und das Admin wurde mit newforms-admin generalüberholt.

Das im Artikel angesprochene Prinzip funktioniert immernoch, ist aber nicht mehr zeitgemäß. Daher freut es mich, dass es nun ein eigenes App für dieses Thema gibt:

django-batchadmin von Brian Beck liefert alles, um multiple Deletes in Djangos Administrationsoberfläche auszuführen und erlaubt dazu noch, es mit eigenen Plugins zu erweitern. Das alles absolut konform mit newforms-admin. Sehr cool!

Getestet habe ich es leider noch nicht, der einzige Knackpunkt den ich auf Anhieb gefunden habe ist, dass django-batchadmin zwingend jQuery benötigt.

Zeig mal den Query

Oft will man schnell mal wissen, was Djangos DB-Api so im Hintergrund treibt, sprich: Welches SQL-Statement wird abgesetzt.

Es gibt viele Apps die das übernehmen, der letzte Schrei derzeit ist Rob Hudsons Django Debug Toolbar. Der Nachteil ist, dass die Ausgabe zumeist im Template stattfindet. Im Quellcode direkt gehts aber auch:

1
2
3
>>> q = Entry.objects.values('id')
>>> print q.query.as_sql()
('SELECT "weblog_entry"."id" FROM "weblog_entry" ORDER BY "weblog_entry"."published" DESC', ())

Die Ausgabe erfolgt dann im Terminal bzw. im Logfile des Webservers. Neat! Smiley:  :-)

Spass mit Newforms-Admin - Automatische Felder

Im letzten Artikel Rowlevel-Permissions ging es darum, wie man Einträge nur vom Benutzer selber bearbeiten lassen kann. Wer den Artikel verfolgt und aufgebaut hat, wird festgestellt haben, dass die ganze Sache einen gewaltigen Haken hatte:

In der Detailansicht konnte das Autorenfeld frei definiert werden, sprich Benutzer A konnte sich für Benutzer B ausgeben und umgekehrt. In diesem Artikel geht es darum, dem eingeloggten Autor automatisch den Artikel zuzuweisen.

Holzhammer

Die einfachste Lösung ist: Zeige das Autorenfeld gar nicht erst an und weise beim Speichern des Artikels dem Autorenfeld den eingeloggten User zu. Das Ganze ist schnell geschehen. Ähnlich wie die "newforms save()" Methode stellt auch ModelAdmin eine Methode zum Überschreiben zur Verfügung:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class EntryAdmin(admin.ModelAdmin):

    # Autorenfeld ausblenden (mit #7937 geht das irgendwann mal einfacher)
    # #7937: http://code.djangoproject.com/ticket/7973
    fields = (
        'title',
        'content',
    )

    # Formular valdidieren, Autor setzen und speichern
    def save_form(self, request, form, change):
        instance = form.save(commit=False)
        # Nur bei neuen Artikeln den User setzen
        if not change:
            instance.author = request.user
        return instance

Fertig ist die ganze Geschichte. Schnell, sicher und kaum zu überlisten.

Nicht ganz so krass

Vielleicht ist das aber auch schon einen Schritt zu weit. In meinem aktuellen Projekt möchte ich es trotzdem dem Benutzer überlassen, den Autor zu setzen, allerdings mit einer Kleinigkeit: Der aktuelle Benutzer soll in der Liste schon vorausgewählt sein.

Das Ganze ist ein wenig mehr "tricky". In ModelAdmin gibt es wiederrum eine Methode, die vor der Anzeige der Felder aufgerufen wird und mit der man ihr Verhalten und Aussehen (Widgets) beeinflussen kann: formfield_for_dbfield.

1
2
3
4
5
6
7
8
9
class EntryAdmin(admin.ModelAdmin):

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

        # Autorenfeld hat als Vorauswahl den aktuellen User
        if db_field.name == "author":
            field.initial = request.user.pk
        return field

In dieser Methode prüfen wir, ob das aktuelle Feld unser "Autorenfeld" ist, dann wird ihm als Startwert (initial) der aktuelle User zugewiesen.

Nächster Haken: Der obige Code funktioniert nicht, in der Methode formfield_for_dbfield hat man keinen Zugriff auf das request-Objekt. Ein wenig Patching muss her:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class EntryAdmin(admin.ModelAdmin):

    def add_view(self, request,  *args, **kwargs):
        self._request = request
        return super(EntryAdmin, self).add_view(request,  *args, **kwargs)

    def change_view(self, request, *args, **kwargs):
        self._request = request
        return super(EntryAdmin, self).change_view(request,  *args, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

        # Autorenfeld hat als Vorauswahl den aktuellen User
        if db_field.name == "author":
            field.initial = self._request.user.pk
        return field

Glücklicherweise ist ModelAdmin flexibel genug, das Problem zu lösen. Die Methoden add_view und change_view werden vor der Anzeige des Formulares aufgerufen und beiden steht das Request-Objekt zur Verfügung. Das ist genau der richtige Ort, das Request-Objekt als globale Eigenschaft (self._request) der Klasse zuzuweisen.

Somit kann man in der formfield-Methode einfach auf das "globale" Request-Objekt zugreifen und mit den paar Handgriffen ist der Benutzer in der Autorenliste vorausgewählt.

Mit ein wenig Javascript kann man auch einfach aus diesen beiden Sachen eins machen:

Und nicht vergessen: Auch hier ist es wieder möglich mit Permissions zu arbeiten. So könnte ein Administrator immer den Autor setzen (zweite Variante), wo ein "normaler" User kein Autorenfeld zu Gesicht bekommt und automatisch gesetzt wird (erste Variante).

Spass mit Newforms-Admin - Rowlevel-Permissions

Newforms-Admin ist schon eine Weile verfügbar und daher wird es Zeit, sich einmal mit den Features auseinander zu setzen. Dieser Artikel ist der Anfang einer Reihe von Tipps und Tricks zu Newforms-Admin, die nacheinander alle aufeinander aufbauen.

Ein oft gewünschtes Feature im Admin-Interface ist "Lasse den Benutzer nur seine eigenen Einträge bearbeiten", anders ausgedrückt: Rowlevel-Permissions. Eine der einfachsten Aufgaben für das neue Admin-Interface.

Fangen wir wieder einmal mit dem Model an, ein kleines Weblog ist ein gutes Beispiel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.db import models
from django.contrib.auth.models import User

class Entry(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.ForeignKey(User)

    def __unicode__(self):
        return self.title

Um das Model mit dem Admin Interface administrieren zu können, ist eine einfache ModelAdmin-Klasse notwendig, bestens aufgehoben in der Datei admin.py (Jahaa):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.contrib import admin
from myproject.weblog.models import Entry

class EntryAdmin(admin.ModelAdmin):
    list_display = (
        'title',
        'author',
    )

admin.site.register(Entry, EntryAdmin)

Soweit nichts spektakuläres, mit ein paar Einträgen von verschiedenen Usern sieht die Übersichtsseite so aus:

Nun zur Aufgabe: Um dem Benutzer nur seine Beiträge bearbeiten zu lassen, lässt Newforms-Admin auf einfache Weise zu, das Queryset für die Listenansicht zu bearbeiten. Füge in deine ModelAdmin-Klasse eine Funktion "queryset" hinzu, die die Einträge anhand der User/Autor-Beziehung filtert:

1
2
3
4
class EntryAdmin(admin.ModelAdmin):
    # ...
    def queryset(self, request):
        return self.model._default_manager.filter(author=request.user)

Das war es auch schon! Jeder Autor sieht ab sofort nur noch seine eigenen Artikel:

Die Sache hat nur einen Haken: Auch Administratoren würden nur ihre eigenen Artikel sehen, irgendwie nicht sehr sinnvoll. Um das Problem zu lösen, schaffen wir uns am einfachsten eine zusätzliche Permission. Erweitere also dein Model wie folgt:

1
2
3
4
5
6
class Entry(models.Model):
    # ...
    class Meta:
        permissions = (
            ('can_view_all', 'Can view all Entries'),
        )

Nach einem "./manage.py syncdb" ist diese neue Permission auch schon verfügbar, die du deinem User zuweisen kannst:

Leider ist nicht genug Voodoo in Django vorhanden, als dass alles automatisch geht. In deiner Queryset-Redefinition musst du noch auf die neue Permission prüfen, etwa so:

1
2
3
4
5
def queryset(self, request):
    # ...
    if request.user.has_perm('weblog.can_view_all'):
        return self.model._default_manager.get_query_set()
    return self.model._default_manager.filter(author=request.user)

Und fertig. "Normale" User können nun nur noch ihre eigenen Beiträge bearbeiten während Administratoren oder User mit der Permission "can_view_all" alle Einträge angezeigt bekommen.

Zu guter letzt noch ein Hinweis: Theoretisch könnte man dieses Verhalten auch umdrehen und eine Permission "Can view only his own entries" (*urgs*) schaffen. Das nur fürs Gedächtnis.

Wiederverwendbare Django-Projekte

Eine der Stärken von Django ist die Wiederverwendbarkeit von einzelnen Applikationen (aka reusable apps). Will man aber ein komplettes Projekt veröffentlichen, sollte man sich auch Gedanken machen, wie man das komplette Projekt möglichst automatisch veröffentlichen kann. Anders ausgedrückt: der User soll möglichst wenig Pfade und Config-Einstellungen von Hand anpassen müssen.

Die verschiedensten Einstellungen eines Django-Projekts befinden sich in der Regel in der settings.py im Hauptverzeichnis deines Projekts, hier sind ein paar kleine Kniffe, wie du diese Einstellung möglichst dynamisch hälst:

Ein Hinweis zuvor: Die settings.py ist eine Python-Datei und wird auch von Python geparst. Es ist keine Ansammlung statischer Definitionen sondern in ihr kann auch eine komplette Programmlogik ausgelagert werden.

1. Dynamische Pfade

Die erste Notwendigkeit vor dem Deployment einer Seite ist es, die Pfade zu den Templates und den statischen Mediendateien anzupassen. Diese Arbeit können wir dem User abnehmen, indem wir sie von vornherein dynamisch festlegen. Grundlage allen ist es, erst einmal zu wissen, welcher Pfad zum Projektverzeichnis führt:

1
2
import os
PROJECT_ROOT = os.path.dirname(__file__)

Die Variable PROJECT_ROOT enthält damit den Pfad zum aktuellen Verzeichnis, sei es /var/www/meinprojekt/ oder unter Windows C:\Dokumente und Einstellungen\username\meinprojekt\.

Damit ist es möglich, die Pfade zu den Templates und den Mediendateien festzulegen. Eine Arbeit der der User vor dem Deployment also nicht mehr erledigen braucht:

1
2
3
4
5
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'site_media')

TEMPLATE_DIRS = (
    os.path.join(PROJECT_ROOT, 'templates'),
)

2. Lass den SECRET_KEY geheim

Oft wird es vom User vergessen, den SECRET_KEY neu zu setzen. Er wird bspw. als Salt für die Passwort-Generierung benutzt. Am sichersten und einfachsten ist es für den User, den SECRET_KEY bei Bedarf sich selbst erstellen zu lassen: (Übernommen vom Byteflow-Projekt)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
try:
    SECRET_KEY
except NameError:
    SECRET_FILE = os.path.join(PROJECT_ROOT, 'secret.txt')
    try:
        SECRET_KEY = open(SECRET_FILE).read().strip()
    except IOError:
        try:
            from random import choice
            SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)])
            secret = file(SECRET_FILE, 'w')
            secret.write(SECRET_KEY)
            secret.close()
        except IOError:
            Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE)

Der kleine Schnipsel prüft, ob eine Variable SECRET_KEY schon definiert wurde. Wenn nicht, erstellt er eine Datei secret.txt im Projektverzeichnis und schreibt darin selbstständig einen zufälligen String. (Der Webserver muss in diesem Fall Schreibrechte für das Projektverzeichniss besitzen.)

3. Don't touch the settings

In der Bildbearbeitung gibt es einen Leitsatz: Don't touch the Pixels. Das bedeutet, wenn man ein Bild bearbeiten will, legt man sich zuvor eine Kopie der Bildebene an oder erstellt einen Snapshot, um im Bedarfsfall auf die Originaldaten zurück greifen zu können.

Mit den Django-Settings verhält es sich ähnlich. Ich füge in alle meine Settings-Dateien am Ende diesen Schnipsel ein:

1
2
3
4
try:
    from local_settings import *
except ImportError:
    pass

Er versucht, eine Datei "local_settings.py" zu finden und übernimmt/überschreibt die Einstellungen aus dieser Datei in die globalen Einstellungen. Diese "local_settings.py" ist der perfekte Ort für die Einstellungen, die vom User in jedem Fall gesetzt werden müssen – die Einstellungen für den Datenbankzugriff etwa.

Der große Vorteil kommt aber erst zum Tragen, wenn man das Projekt regelmäßig aus einem VCS aktualisiert. Da im Idealfall keine Daten in der ursprünglichen "settings.py" geändert werden, gibt es keine Kollisionen mit Updates. Der User kann also das Projekt regelmässig aus dem VCS aktualisieren, ohne jedes Mal die Einstellungen neu schreiben zu müssen.

Das waren drei kleine Kniffe, die das Deployment eines Projekts für den User sehr erleichtern. Das Ganze lässt sich noch beliebig fortführen, ist aber in den meisten Fällen projekt-abhängig. Hab ich noch etwas wichtiges vergessen, dann schreibt doch einen Hinweis in die Kommentare. Smiley:  :-)

Generische Dateinamen für generische Apps

In Django gibt es nur wenige Konventionen, wie man seine Dateinamen (bzw. Python-Module) benennen soll. Wächst das Django-Projekt wird man nicht drumrum kommen, Code in eigene Module auszulagern. So hat es sich z.B. eingebürgert, forms-Formulare in eine Datei "forms.py" der jeweiligen Applikation legen.

In #django-de gab es in letzter Zeit häufig Fragen und Diskussionen zu diesem Thema und nun hat Arne Brodowski sich die Mühe gemacht, diese Dateinamen einmal aufzulisten und zu beschreiben:

http://www.django-hosting.de/wiki/BestPracticesDateinamen/

Um es noch einmal klar zu stellen: Niemand wird gezwungen, diese Dateikonventionen zu verfolgen. Wer aber sein Projekt oder Applikation veröffentlichen will, sie möglichst wiederverwendbar gestalten will, tut gut daran, sich an diese Quasi-Standards zu halten. Diese kommen auch nicht von irgendwo sondern sind aus Erfahrung vieler Open-Source Projekte entstanden.

Danke Arne für die Liste, vielleicht währe es eine Gute Idee, wenn diese Informationen auch im offiziellen Django-Wiki Einzug halten würden.

Newforms AdminOptions besser strukturieren

Wer in den letzten Tagen seine schon etwas ältere Django-trunk Version aktualisiert hat, dürfte sein blaues Wunder erlebt haben, positiv oder negativ. Unter anderem hat Newforms Admin ja endlich Einzug gehalten.

Anders als bisher werden Einstellungen der Models nun nicht mehr in einer Subklasse Admin des Models definiert, sondern in einer eigenen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Entry(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(blank=True, db_index=True)
    content = models.TextField()
    published = models.DateTimeField(auto_now_add=True)

class EntryAdmin(admin.ModelAdmin):
    save_on_top = True
    list_select_related = True
    date_hierarchy = 'published'
    list_display_links = ('title')
    search_fields = ('title', 'content')

admin.site.register(Entry, EntryAdmin)

Nun gibt es prinzipiell zwei Möglichkeiten, diese Admin-Optionen an die Models zu "binden":

  • autodiscover() durchsucht die Apps nach einer Datei "admin.py" und importiert dieses Modul automatisch.

  • Per Hand und überall mittels "admin.site.register('model', 'options')" wie im obigen Beispiel.

Ich bevorzuge den zweiten Weg, da ist weniger Voodoo im Spiel und ich kann penibel festlegen, welches App im Admin erscheint und welche Optionen ihm zugewiesen werden.

Bei meinem aktuellen Projekt habe ich dafür eine Basisklasse "DefaultModelAdmin" für alle ModelAdmins erstellt und leite davon alle anderen ModelAdmin-Klassen ab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# File: apps/adminprefs/defaults
from django.contrib import admin

class DefaultModelAdmin(admin.ModelAdmin):
    save_on_top = True
    list_select_related = True

# File: models.py/admin.py
from django.contrib import admin
from adminprefs.defaults import DefaultModelAdmin

class EntryAdmin(DefaultModelAdmin):
    date_hierarchy = 'published'
    list_display_links = ('title')
    search_fields = ('title', 'content')

admin.site.register(Entry, EntryAdmin)

Das ist nicht nur DRY sondern wird in Zukunft auch helfen, wenn einmal ein zentraler Eingriff für alle Admin-Sachen nötig ist.

ModelsForms mit Choices validiert nicht korrekt

Um es vorweg zu nehmen, dies ist die Beschreibung eines Bugs in Django. Ja, dafür gibt es schon ein Ticket und nein, es ist derzeit noch nicht im Trunk gefixt.

Oki, wer bis hierhin durchgehalten hat, weiter gehts: Smiley:  :-)

Angenommen wir haben dieses simple Model, die Werte des Testfeldes bestehen aus den vorher definierten Choices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.db import models

CHOICES = (
    (u'Bube', u'Bube'),
    (u'Dame', u'Dame'),
    (u'Koenig', u'Koenig'),
    (u'Ass', u'Ass'),
)

# Das Model
class TestModel(models.Model):
    testfeld = models.CharField(max_length=30, choices=CHOICES)

Um ein simples Formular daraus zu erstellen ist ModelForms ideal:

1
2
3
4
5
6
from testprojekt.models import TestModel
from django import forms # Django 1.0 Alpha wohooo!

class TestForm(forms.ModelForm):
    class Meta:
        model = TestModel

Einfacher gehts kaum, mit den paar Zeilen hat man schon ein komplettes Formular parat. Aber dass wußtest du sicher schon, also weiter. Basteln wir uns eine einfache Ausgabe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> f = TestForm()
>>> f.as_ul()

# Beispielausgabe
<select name="testfeld" id="id_testfeld">
    <option value="Bube">Bube</option>
    <option value="Dame">Dame</option>
    <option value="Koenig">Koenig</option>
    <option value="Ass">Ass</option>
</select>

Alles korrekt bis hier hin. Angenommen das Formular wird nun auf der Webseite angezeigt, der User wählt "Bube" und schickt das Formular ab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> f = TestForm(data={'testfeld': 'Bube'})
>>> f.as_ul()

<select name="testfeld" id="id_testfeld">
    <option value="Bube" selected="selected">Bube</option>
    <option value="Dame">Dame</option>
    <option value="Koenig">Koenig</option>
    <option value="Ass">Ass</option>
</select>

>>> f.is_valid()
True

Das Formular ist valide, weil "Bube" ja eine korrekte Auswahl für das Feld 'testfeld' ist. Aber zur Erinnerung: SELECT-Felder in HTML sind auch nur stinknormale Datenfelder und der Browser prüft nicht, ob der abgesendete Wert auch des Feldes auch vorher in den SELECT-Options definiert wurde. Ändern wir also mal die Werte der Formulardaten:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> f = TestForm(data={'testfeld': 'Boeses Maedchen'})
>>> f.as_ul()

<select name="testfeld" id="id_testfeld">
    <option value="Bube">Bube</option>
    <option value="Dame">Dame</option>
    <option value="Koenig">Koenig</option>
    <option value="Ass">Ass</option>
</select>

>>> f.is_valid()
True

Wem das nicht schlimm erscheint: Diese Daten speichert das Formular leider auch in die Datenbank, das Model selbst validiert also nicht noch einmal nach.

1
2
3
>>> objekt = f.save()
>>> objekt.testfeld
u'Boeses Maedchen'

Ganz fatal in meinen Augen. Zur Erinnerung, eigentlich sollte das testfeld im Model nur die Daten Bube, Dame, Koenig und Ass annehmen.

Wie kann man eigentlich "fremde" Daten in ein Select-Feld schreiben? Nichts einfacher als das, die bekannte Firefox-Extension "Web Developer Toolbar" hat eine eigene Funktion dafür:

Mein derzeitiger Lösungsvorschlag sieht so aus, dass ich einfach mit einer clean-Methode auf das Vorhandensein der Eingabe in den Choices prüfe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from testprojekt.models import TestModel, CHOICES
from django import forms

class TestForm(forms.ModelForm):

    def clean_testfeld(self):
        if not self.cleaned_data['testfeld'] in dict(CHOICES):
            raise forms.ValidationError(u'%s is not a valid choice' % (self.cleaned_data['testfeld']))            
        return self.cleaned_data['testfeld']

    class Meta:
        model = TestModel

Dann klappts auch wie gewünscht.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> f = TestForm(data={'testfeld': 'Boeses Maedchen'})
>>> print f.is_valid()
False
>>> f.as_ul()
errorlist: "Boeses Maedchen is not a valid choice"
<select name="testfeld" id="id_testfeld">
    <option value="Bube">Bube</option>
    <option value="Dame">Dame</option>
    <option value="Koenig">Koenig</option>
    <option value="Ass">Ass</option>
</select

Durch das derzeitge Release von Django 1.0 Alpha gehe ich aber mal davon aus, dass dieser Bug in nächster Zeit gefixt wird. Ein Patch exisitiert ja schon.

Wie bei newforms die Validierung abläuft

Djangos newforms Formularen stehen 3 Methoden zur Verfügung, Eingabedaten zu prüfen.

  • Feld.clean(): Die clean-Methode des Feldes selbst. Django bringt für die jeweiligen Felder schon grundsätzliche Prüfungen mit. In ein IntegerField dürfen eben nur Ziffern eingegeben werden und ein EmailField prüft die korrekte Syntax der E-Mail-Adresse.

In der Regel ist es nicht notwendig, bestehende Feld-Clean-Methoden zu ändern, denn dafür gibt es:

  • Formular.clean_<feldname>(): In dieser Funktion kannst du penibel festlegen, welche Daten für das Feld valide sind und einen Fehler ausgeben oder diese Daten ändern.

  • Formular.clean(): Diese Funktion wird am Ende ausgeführt, wenn alle Felder einzeln schon geprüft worden sind.

In allen Funktionen hat das Formular stets Zugriff auf die eingegeben Daten, denn diese stehen im Dict self.cleaned_data. Wofür ist also die "globale" clean() Methode gut?

Dazu ist es wichtig zu wissen, wie der Ablauf der Validierung von Newforms-Formularen stattfindet. Vergleiche einmal dieses Formular und dessen Ausgabe:

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django import newforms as forms

class Form(forms.Form):
    name = forms.CharField()
    plz = forms.IntegerField()
    ort = forms.CharField()

    def clean_name(self):
        print "Vorhandene Daten bei 'name': %s" % self.cleaned_data
        return self.cleaned_data['name']

    def clean_plz(self):
        print "Vorhandene Daten bei 'plz': %s" % self.cleaned_data
        return self.cleaned_data['plz']

    def clean_ort(self):
        print "Vorhandene Daten bei 'ort': %s" % self.cleaned_data
        return self.cleaned_data['ort']

    def clean(self):
        print "Die Formular clean() Methode. Spätestens jetzt sind alle Daten" \
              "vorhanden: %s" % self.cleaned_data
        return self.cleaned_data

test_data = {'name': 'Martin', 'plz': '18581', 'ort': 'Putbus'}
f = Form(test_data)
f.as_ul()

Ausgabe:

1
2
3
4
5
6
>python -u test.py
Vorhandene Daten bei 'name': {'name': u'Martin'}
Vorhandene Daten bei 'plz': {'name': u'Martin', 'plz': 18581}
Vorhandene Daten bei 'ort': {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581}
Die Formular clean() Methode. Spätestens jetzt
sind alle Datenvorhanden: {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581}

Zur Erläuterung. Die Validierung im Formular geht der Reihe nach, anhand der Felder von oben nach unten. In diesem Formular ist es also dieses Schema:

  1. 'name' den Wert aus den Daten zuweisen
  2. name.clean() - Die interne clean-Funktion eines CharFields
  3. f.clean_name() - Unsere selbst definierte clean-Funktion für 'name'
  4. 'plz' den Wert aus den Daten zuweisen
  5. plz.clean() - Die interne clean-Funktion eines IntegerFields
  6. f.clean_plz() - Unsere selbst definierte clean-Funktion für 'plz'
  7. 'ort' den Wert aus den Daten zuweisen
  8. ort.clean() - Die interne clean-Funktion eines CharFields
  9. f.clean_ort() - Unsere selbst definierte clean-Funktion für 'ort'
  10. f.clean() - Die "globale" Clean-Methode wird erst am Ende ausgeführt.

In der Ausgabe siehst du auch, das dir in der Funktion "clean_name" (unser erstes Feld) noch gar keine Werte für die Felder "plz" und "ort" zur Verfügung stehen. Nochmal: die Validierung und Zuweisung der Daten läuft von oben nach unten, in der Reihenfolge der Felder.

Warum kann man also schon im ersten Feld nicht auf alle Eingabedaten zugreifen? Hypothese: Wozu soll das Formular auch alle Datenfelder durchlaufen, wenn schon im ersten Feld 'name' ein Fehler (ValidationError) auftreten kann.

Aha, toll

Ok, kommen wir endlich zum springenden Punkt für diesen Artikel. In der Regel wird ein Formular nicht nur pro Feld validiert sondern auch in Kombination. Beispiele währen:

  • 2 Passwort-Felder müssen das selbe Passwort haben
  • Möchte ich per Lastschrift zahlen, muss ich auch Kontodaten hinterlegen
  • Wünsche ich Antwort per E-Mail, muss ich auch eine E-Mail-Adresse hinterlegen, sonst reicht mein Name

Und so weiter, das ganze hängt ja vom Anwendungsfall ab und die Möglichkeiten so eines Formulares sind unbegrenzt. Nun endlich der springende Punkt, und ich würde das ganze hier nicht schreiben, wenn es nicht ein häufiges (Anfänger)Problem währe:

Willst du mehrere Felder miteinander vergleichen gehört diese Logik in die clean()-Funktion des Formulares, nicht in die des Feldes! Der Grund dafür steht ja schon oben.

Nun aber das Aber. Der Grund warum so viele Leute die Validierungslogik in eine clean_<feldname> Funktion packen – obwohl es mehrere Felder betrifft – ist, dass Fehlermeldungen aus der clean() Funktion immer ganz oben, vor allen Feldern angezeigt werden. Die so genannten non_field_errors, also Fehlermeldungen die nicht an ein Feld gebunden sind.

Hier ein Beispiel dass die Felder 'ort' und 'name' miteinander prüft:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django import newforms as forms

class Form(forms.Form):
    name = forms.CharField()
    plz = forms.IntegerField()
    ort = forms.CharField()

    def clean(self):
        if self.cleaned_data['name'] == 'Meier' and \
           self.cleaned_data['ort'] == 'Berlin':
            raise forms.ValidationError('Es gibt keinen "Meier" in Berlin.' \
                  'Bitte legen Sie sich einen anderen Namen zu.')

test_data = {'name': 'Meier', 'plz': u'10627', 'ort': 'Berlin'}
f = Form(test_data)
print f.as_ul()

In der Ausgabe des Formulares steht der Fehler ganz oben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!--
    non_field_erros Fehlermeldung
    (normalerweise ganz oben)
-->
<li><ul class="errorlist"><li>Es gibt keinen "Meier" in Berlin.Bitte legen Sie sich einen anderen Namen zu.</li></ul></li>

<!--
    Weiter gehts mit den Feldern, die aber
    keine eigenen Fehlermeldungen besitzen
-->
<li><label for="id_name">Name:</label> <input type="text" name="name" value="Meier" id="id_name" /></li>
<li><label for="id_plz">Plz:</label> <input type="text" name="plz" value="10627" id="id_plz" /></li>
<li><label for="id_ort">Ort:</label> <input type="text" name="ort" value="Berlin" id="id_ort" /></li>

Möchte man aber im Nachhinein noch einem Feld einen Fehlertext zuweisen, so geht das einfach über das Dict self.errors[<feldname>] (es erwartet für das Feld ein Liste oder Tupel, über die es iterieren kann):

1
2
3
4
5
6
7
8
#!/usr/bin/env python
class Form(forms.Form):
    # ...
    def clean(self):
        if self.cleaned_data['name'] == 'Meier' and \
           self.cleaned_data['ort'] == 'Berlin':
            self.errors['name'] = ('Es gibt keinen "Meier" in Berlin.',)
            raise forms.ValidationError('Es sind Fehler aufgetreten:')

In der Ausgabe hat unser Feld nun seine eigene Fehlermeldung:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!--
    non_field_erros Fehlermeldung
    (normalerweise ganz oben)
-->
<li><ul class="errorlist"><li>Es sind Fehler aufgetreten:</li></ul></li>

<!--
    Unser Namensfeld mit einer eigenen Fehlermeldung
-->
<li>
    <ul class="errorlist"><li>Es gibt keinen &quot;Meier&quot; in Berlin.</li></ul>
    <label for="id_name">Name:</label> <input type="text" name="name" value="Meier" id="id_name" />
</li>
<li><label for="id_plz">Plz:</label> <input type="text" name="plz" value="10627" id="id_plz" /></li>
<li><label for="id_ort">Ort:</label> <input type="text" name="ort" value="Berlin" id="id_ort" /></li>

So weit... viel Spass weiterhin mit newforms. Smiley:  :-)

Python difflib Praxisbeispiel

Für eine Communityseite möchte ich, wenn ein Beitrag geändert wird, dem Vorautor eine E-Mail mit den Änderungen schicken. Die klassische Ausgabe eines Diffs gefällt mir besonders gut. Hier ist ein kleines Beispiel wie das Pythonmodul difflib zu gebrauchen ist:

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import difflib

a = """
Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed
ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
tincidunt augue, quis ullamcorper lorem pede a ante.
"""

b = """
Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed
ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
tincidunt augue, quis ullamcorper lorem pede a onte!
"""

d = difflib.unified_diff(
    a.splitlines(),
    b.splitlines(), 
    'Original',
    'Current',
    lineterm=''
)

print '\n'.join(d)

Und die Ausgabe:

1
2
3
4
5
6
7
8
9
--- Original 
+++ Current 
@@ -1,4 +1,4 @@

-Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed
+Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed
 ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
-tincidunt augue, quis ullamcorper lorem pede a ante.
+tincidunt augue, quis ullamcorper lorem pede a onte!

Wer eher nach einer farbigen, detailierten Ausgabe wie bei Trac sucht (hier im Beispiel übernimmt Pygments im Nachhinein das Highlighting), sollte sich auch das HtmlDiff Modul anschauen.

Zusätzliche Templatedaten in newforms-Formulare einfügen.

Ok, das ist schon ein kryptischer Titel aber es ist etwas ganz Einfaches gemeint.

Djangos newforms Formulare lassen sich auf einfache und schnelle Weise mit den Methoden as_ul(), as_p() und as_table() ausgeben. Leider werden die Felder dann aber der Reihe nach ausgegeben, einen Zwischentitel oder ähnliches einzufügen ist dann nicht mehr möglich.

1
2
3
<ol>
{{ form.as_ul }}
</ol>

Angenommen im Formular befinden sich folgende Felder:

  • username
  • emailadresse
  • passwort
  • passwort (wiederholung)
  • Vorname
  • Nachname
  • Homepage-URL

Wobei die ersten 4 Felder Pflichtfelder sind, die letzten 3 aber optional. Über diese letzten 3 Felder möchte ich eine Titelzeile "Die folgenden Felder sind optional" setzen.

Nun ist es einfacher weise möglich, das Formular per Hand aufzubauen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<ol>
<li>
  <label>{{ form.username.label_tag }}</label>
  {{ form.username }}
  {{ form.username.help_text}}
</li>    
...    
<li class="title">Die folgenden Felder sind optional</li>
<li>
  <label>{{ form.first_name.label_tag }}</label>
  {{ form.first_name }}
  {{ form.first_name.help_text}}
</li>    
...
</ol>

Sicherlich erreicht man so die maximale Flexibilität aber es ist auch fehleranfällig wenn sich ein Feld(name) ändert und es muss immer Hand angelegt werden, wenn ein neues Feld hinzukommt oder entfernt wird. Also zurück zu den direkten Formular Ausgabemethoden:

1
2
3
<ol>
{{ form.as_ul }}
</ol>

Über die Felder im Formular lässt sich auch iterieren! form.as_ul() ist in der Ausgabe identisch zu diesem Schnipsel:

1
2
3
4
5
6
7
8
9
{{ form.non_field_errors }}
{% for field in form %}
<li>
    {% if field.errors %}{{ field.errors }}{% endif %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}{{ field.help_text }}{% endif %}
</li>
{% endfor %}

Ich wollte ja über die letzten 3 Felder eine Titelzeile "Die folgenden Felder sind optional" setzen. Das erste der 3 Felder ist "Vorname" und da wir wissen wie dieses Feld heißt, können wir auch darauf prüfen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{ form.non_field_errors }}
{% for field in form %}

{% ifequal field.label "First name" %}
<li class="spacer">
    Die folgenden Felder sind optional.
</li>
{% endifequal %}

<li>
    {% if field.errors %}{{ field.errors }}{% endif %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}{{ field.help_text }}{% endif %}
</li>
{% endfor %}

Auf diese Weise lassen sich einfach Daten vor, nach oder selbst in bestimmte Felder einfügen – ohne dass die Flexibilität von newforms Formularen gefährdet ist.

APPEND_SLASH und PREPEND_WWW im Webserver

Djangos per Default aktive Middleware CommonMiddleware stellt zwei Parameter zur Verfügung, mit denen sich das Verhalten aller URLs ändern lässt.

  • PREPEND_WWW leitet eine Anfrage auf eine Seite ohne voran gestelltes www "http://example.org/" automatisch zur Domain mit www "http://www.example.org/" um.
  • APPEND_SLASH fügt der URL, wenn sie nicht mit einem Slash endet, diesen an. Das ist auch das normale Verhalten der meisten Webserver, wenn ein Verzeichnis angefordert wird.

Beides ist eine Aufgabe die der Webserver schneller und wahrscheinlich resourcenschonender erledigen kann. Wer mod_rewrite seinem Apache zur Verfügung stellt, kann diese beiden Optionen auch direkt erledigen lassen. Ohne den mod_wsgi/mod_python Unterbau und ohne dass eine Codezeile von Django ausgeführt wird.

Dieser Schnipsel gehört in die VHost deiner Domain – oder je nach Aufbau deines Django-Projekts auch in eine .htaccess Datei des entsprechenden Ordners:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Voodoo-Engine starten    
RewriteEngine On
#RewriteLog /path/to/rewrite.log
#RewriteLogLevel 9

# Prepend WWW
RewriteCond %{HTTP_HOST} !^www\.
RewriteRule ^/(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]

# Append Slash
RewriteCond %{REQUEST_URI} !\.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !.*/$
RewriteRule ^(.*)$ $1/ [R=301,L]

Nützlich ist dieser Schnipsel auch allen, die Djangos Flatpages einsetzen. Wenn der User eine URL besucht, die auf eine Flatpage zeigt, aber den Slash am Ende vergisst, bekommt er nur einen 404-NotFound Fehler zurück. Ein Bug der schon etwas länger diskutiert wird und hoffentlich in Zukunft auch gefixt wird. Einen Patch gibt es jedenfalls schon.

Inhalte von Passwort-Feldern nicht wieder anzeigen

Das Passwort-Widget aus Django Newforms zeigt bei einer erneuten Ausgabe den Formulares den Inhalt wieder an. Das ist zum einen sicherheitskritisch: in einem ungünstigen Fall könnte ein Dritter die Daten des Formulares erkunden und das Passwort sehen (Der Wert eines Passwort-Feldes ist im HTML-Quelltext klar zu sehen), zum anderen soll der User sowieso beide Passwörter neu eingeben.

Als Beispiel dient dieses einfache Registrierungsformular:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class RegistrationForm(forms.Form):
    name = forms.CharField(label='Name')
    password = forms.CharField(label='Passwort', widget=forms.PasswordInput)
    password_confirm = forms.CharField(label='Passwort (Bestätigung)', widget=forms.PasswordInput)

    # Prüfe hier, ob überhaupt korrekte Passwörter eingegeben wurden
    # def clean_password(self):
    # ... bspw. auf mindestens 8 Zeichen prüfen, wovon mindestens 1 Ziffer dabei ist

    def clean(self):
        if self.cleaned_data.get('password') != self.cleaned_data.get('password_confirm'):
            raise forms.ValidationError('Die beiden Passwörter müssen identisch sein')
        return self.cleaned_data

Angenommen wir klicken einmal auf den Submit-Button so würde das Formular (da in unserem Fall die Passwörter nicht identisch sind) wieder angezeigt:

Das Passwort wird wieder angezeigt.

Das PasswordInput-Widget bietet aber eine (nicht dokumentierte?) Funktion, den Inhalt des Feldes nicht mehr, genauer gesagt: nie, anzeigen zu lassen. Füge dem Wigdet das Attribut render_value=False hinzu und das Passwort-Feld ist bei jedem Rendern des Formulares leer.

Beispiel:

1
2
3
4
5
6
class RegistrationForm(forms.Form):
    name = forms.CharField(label='Name')
    password = forms.CharField(label='Passwort', widget=forms.PasswordInput(render_value=False))
    password_confirm = forms.CharField(label='Passwort (Bestätigung)', widget=forms.PasswordInput(render_value=False))

    # ...

Templatefilter allen Templates zur Verfügung stellen

Das kennst du sicher auch. Du hast dir ein paar hübsche Templatefilter geschrieben, die du praktisch in jedem Template brauchst. Ein Markdown-Filter oder Djangos I18N-Tags währen solche Kandidaten. Nun ist es super nervig, diese Templatefilter jedem Template zur Verfügung zu stellen:

1
{% load i18n mymarkdown imagefilter ... %}

Was bei einer Hand voll Templates noch in Ordnung ist, artet im späteren Entwicklungsstadium zu einer Kaffee-vernichtenden {%load%}-Orgie aus. Ganz zu schweigen davon, dass sich der Templatefilter-Name auch ändern könnte.

Django bietet aber eine Möglichkeit, diese Templatetags zu den "globalen" Templatetags hinzuzufügen, sie stehen damit allen Templates sofort zur Verfügung. Füge dazu irgendwo* deinem Projektcode diesen Block hinzu:

1
2
3
4
from django.template import add_to_builtins  
add_to_builtins('django.templatetags.i18n')
add_to_builtins('my_project.lib.templatetags.mymarkdown')
add_to_builtins('my_project.lib.templatetags.imagefilter')

* Wenn du keine Applikation mit dem Namen »irgendwo« hast, setze diesen Codeblock in die __init__.py die den entsprechenden templatetags Ordner beinhaltet.

Danke Jannis für den Tipp.

Django Admin: Felder in Reihe anzeigen

Ein kurzer Tipp für zwischendurch, er ist zwar auch dokumentiert aber der eine oder andere weiß es vielleicht noch nicht.

Das fields Attribut lässt dich die Formular-Felder ganz nach Belieben anordnen, und das geht sogar reihenweise; man verpasst den Feldern nur eine eigene Tupel. Beispiel:

1
2
3
4
5
6
7
fields = (
    (_('Essentials'), {
        'fields': (
            'title', # Zeile 1
            ('status', 'category', 'language'), # Zeile 2
            'content', # Zeile 3
            # ...

Daraus wird in der Ausgabe:

Multiple Aktionen in Djangos Administration

Wenn du Djangos Administrationsoberfläche nutzt, wirst du dich vielleicht schon einmal geärgert haben, dass man für jede Aktion eines Element erst das Element selbst aufrufen muss. Beispiel: Möchtest du einen Beitrag löschen, musst du ihn anklicken und in der Detailansicht unten links auf den "Löschen" Button klicken.

Nicht weiter tragisch, spätestens wenn du aber 10 oder mehr Beiträge löschen willst, (Stichwort: Spam-Kommentare) wird dich dieses Verhalten nerven.

Dieser Beitrag soll einen Ansatz zeigen, Aktionen auf multiple Elemente anzuwenden.

Weiter lesen "Multiple Aktionen in Djangos Administration" »

Vorschaubilder in Djangos Administration

Vielleicht hast du ja eine Bildergalerie auf deiner Seite und ärgerst dich im Admin, das du kaum Übersicht über die Bilder hast. Dieses Tutorial zeigt, wie man kleine Vorschaubilder (Thumbnails) in Djangos Administrationsoberfläche anzeigt.

Weiter lesen "Vorschaubilder in Djangos Administration" »

Beim Bücherkauf noch mehr sparen

Wer öfter englischsprachige Bücher kauft, zumeist sind das ja Fachbücher, sollte auch einmal einen Blick auf amazon.com werfen. Dort habe ich gerade die Django-Referenz "The Definitive Guide to Django: Web Development Done Right" erstanden.

Amazon.de verkauft dieses Buch für 38,99€, bei amazon.com steht es derzeit für US$29.69 (ca. 20,40€) in den Regalen. Eine Ersparnis von fast 50%. Leider – oder natürlich – versendet amazon.com nicht kostenfrei nach Europa sondern legt eien Pauschale von $7,98 für Versand und Zoll oben drauf.

Weiter lesen "Beim Bücherkauf noch mehr sparen" »

Lightbox für alle (Django Template-Filter)

Seit Jahr und Tag verwende ich das bekannte Lightbox um die Großversion von Vorschaubildern anzuzeigen. Dazu muss lediglich das Attribut rel="lightbox an den Link gesetzt werden. Leider war ich nicht ganz so konsequent in der Nutzung so das einige Bilder in Blogbeiträgen Lightbox unterstützen, bei anderen habe ich es vergessen.

Diesem Umstand habe ich nun mit einem kleinen Template-Filter nachgeholfen:

1
2
3
4
5
6
7
8
9
import re
from django.utils.encoding import smart_unicode

r_lightbox = re.compile('<a (?=[^>]*\.(jpg|gif|png))(?![^>]*lightbox)')
s_lightbox = '<a rel="lightbox" '

@register.filter
def lightbox(content):
    return r_lightbox.sub(s_lightbox, smart_unicode(content))

Der Aufruf in meinem Template schaut dann so aus:

1
{{ Entry.body_text|markdown|lightbox }}

Der Filter erkennt, ob ein Link auf eine .jpg, .gif oder .png Datei leitet und fügt – wenn nicht schon vorhanden – das rel="lightbox" Attribut hinzu. Ein kleiner Bug steckt noch drin, so würde z.B. ein Link zu http://www.jpgmag.com/ auch als Bild gewertet werden (da .jpg im String vorkommt), aber damit kann ich leben. Smiley:  :-)

Woher das Wetter von meiner Seite stammt

Vielleicht ist jemanden schon die Wetterinfo meines Heimatortes oben rechts auf jeder Seite aufgefallen. Im folgenden zeige ich euch, wie ich euch, wie ich diese Daten abfrage und in meinem Django-App speichere.

Der eine oder andere wird sich sicher fragen: "Was interessiert mich das Wetter irgendeines beknackten Dorfes am Rande Deutschlands?" aber das ist hier nun mal meine Homepage und hier bestimme ich. Smiley:  :-) Die hier vorgestellte Applikation lässt aber beliebig viele Wetterstationen abfragen so dass man theoretisch ein zweites wetter.com|de bauen könnte.

Wetterdienstdaten sind – wenn es nicht irgendeine Flashbox halb gefüllt mit Werbung ist – in der Regel nicht kostenlos, weather.com macht eine Ausnahme und stellt seine Daten über eine REST-Schnittstelle frei zur Verfügung. Einzige Gegenleistung: Ein Link zur Homepage sowie das Logo muss angezeigt werden.

Weiter lesen "Woher das Wetter von meiner Seite stammt" »

Ein paar Template-Helfer (Templatetags)

Im Zuge der Neuordnung meiner Seite habe ich die Codebasis von Grund auf überholt. Diese Seite war mein erstes Projekt mit Django und natürlich sammelt sich viel Müll an. Von ehemals über 30 Template-Tags und Filtern sind nur noch eine Hand voll über geblieben.

Django bringt von zu Hause schon eine mächtige Template-Engine mit, einige Kleinigkeiten fehlen noch. Nichts spektakuläres aber vielleicht ist der eine oder andere interessiert:

Weiter lesen "Ein paar Template-Helfer (Templatetags)" »

MovableType Daten nach Django importieren

Ich habe hier kräftig aufgeräumt und unter anderem mein Weblog www.freakydog.de in diese Seite importiert. mahner.org wurde schon seit über 3 Monaten nicht mehr aktualisiert was in erster Linie daran lag, dass ich nicht genug Content für 2 Blogs habe.

Übrigens lebt www.freakydog.de weiter, die Seite habe ich in Windeseile nach Django portiert und nun wird dieses Weblog auf beiden Seiten angezeigt, beides mit der selben Django-Instanz.

Zurück zum Import: Mein altes Blog lief unter MovableType (MT) und ich zeige euch nun grob, wie ich die Daten importiert habe. Das ganze funktioniert natürlich nicht mit jedem Model. Es soll aber einen Denkanstoß geben, wie man die Daten – mögichst automatisch – aus MT exportiert und in eine Django-Tabelle importiert.

Weiter lesen "MovableType Daten nach Django importieren" »

Wikipedia-Content abfragen

Wikipedia hat einen simplen aber effektiven Schutz, um sich vor Content-Diebstahl zu schützen: eine Abfrage ohne "User-Agent" erhält nur einen praktisch nichts-sagenden Fehler:

Failed to open stream: HTTP request failed! HTTP/1.0 403 Forbidden

Anders ausgedrückt: wer mit dem Browser auf Wikipedia surft hat kein Problem, wer aber einen Grabber nutzt um Wikipedia-Content abzufragen kriegt keine Ergebnisse.

Nun ist es aber gerade kein Content-Diebstahl wenn man Wikipedia-Texte auf seiner Homepage zitiert, egal ob in Auszügen oder als Ganzes – solange die Herkunft bzw. der Autor genannt wird. (Diesen Umstand wissen die meisten Wikipedia-Autoren aber selbst auch nicht.)

Weiter lesen "Wikipedia-Content abfragen" »

Warum Web-APIs in Deutschland nicht funktionieren

Ich binde auf dieser Webseite ein knappes dutzend Webservices über APIs (Schnittstellen) ein und dabei fällt mir eins auf: kein einziger Service stammt aus Deutschland, nur einer aus Europa. Und dass, wo doch gerade hier in Deutschland der Begriff „Web2.0“ an jeder Straßenecke klebt. Mal ehrlich: ein Web2.0 ist in Deutschland doch gar nicht existent!

Wenn ich eine Webseite bastel, in der ein User sein Video hochladen kann, ein anderer dieses kommentieren kann und es schon das Höchste ist, wenn man diese Kommentare auch noch als RSS-Feed abonnieren kann, dann ist das kein Web2.0.

Ich möchte dieses Begriff auch nicht weiter zerpflücken oder gar versuchen zu deuten. Ich mag dieses „Web2.0“ Gedöns auch nicht, dieser Begriff wird einfach zu oft von Menschen missbraucht, die überhaupt keine Ahnung von der Materie haben aber leider oftmals in den sogenannten „Entscheidungspositionen“ stehen und (altes) Wasser als Wein verkaufen.

Weiter lesen "Warum Web-APIs in Deutschland nicht funktionieren" »

Keine Lost-Folgen mehr verpassen

Ich bin ein fanatischer Lost-Gucker und diesen Freitag startet endlich die dritte Staffel auf Premiere. Leider hat Premiere die Angewohnheit sehr, sehr oft Sendungen zu wiederholen und damit ich alle Sendungstermine im Überblick behalte habe ich mir einen kleinen Service gebastelt, der (täglich aktualisiert) alle Lost-Folgen der nächsten Woche anzeigt. Diesen Service findet ihr auf mahner.org/lost.

Ich denke auch, dass es viele interessiert, wie das ganze funktioniert. Deswegen gebe ich euch hier einen Überblick, wie ich diesen Service aufgebaut habe.

Weiter lesen "Keine Lost-Folgen mehr verpassen" »

Kaputte Sonderzeichen in Django

Zur Verwaltung dieser Seite setze ich das Python-Framwork django ein welches ich etwa wöchentlich direkt via SVN aus dem Trunk ziehe. (Wenn du bis jetzt nichts verstanden hast, wird dich der Rest auch nicht interessieren. Smiley:  ;-)

Weiter lesen "Kaputte Sonderzeichen in Django" »