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:  :-)

Firefox cached keine Dateien via SSL

Das Trac-System auf django-de.org erschien mir immer sehr langsam. 5 Sekunden für eine Seite war die Regel. Trotz dem die Response-Headers brav einen ETag aufwiesen macht Firefox 3 einfach kein Conditional Get, sprich jede Datei (Bilder, Stylesheets etc.), egal wie alt sie sind, werden bei jedem Request neu geladen.

Der Grund war dann doch schnell gefunden: Das soll so! Firefox speichert keine Dateien die über https kommen auf der Platte. In gewisser Hinsicht ein Sicherheitsgewinn, für mich in erster Linie aber nervig, da ich Ticketsysteme recht häufig nutze, und diese zumeist über SSL/TLS gehandelt werden.

Die Lösung des Problems ist auch schnell gefunden: In den globalen Einstellungen about:config setzt man einfach browser.cache.disk_cache_ssl von false auf true und schon cached Firefox auch Dateien von SSL-Quellen auf der Platte.

Ob das ein Sicherheitsrisiko darstellt, muss jeder für sich selbst entscheiden.

Bugreport und Diskussion auf bugzilla.mozilla.com.

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.