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

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.

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:

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. ;-)

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