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> |
Frettchen an die Macht?
Schockierend! Ist mir doch kürzlich aufgefallen, dass auf den neuen EU-Heimtierausweisen ein Frettchen neben einem Hund und einer Katze abgebildet ist. Sind Frettchen wirklichen schon so populär? Ich kenne niemanden, der ein Frettchen besitzt oder es auch nur plant. Strange...

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 |

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.

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 |

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. ![]()
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! ![]()
Merkzettel für Git
Ein paar Stichpunkte zum Umgang mit git; in erster Linie als Gedankenstütze für mich:
- Der Master Branch ist der stabile Zweig. Wenn du ein Feature hinzufügen willst, erstelle einen neuen Branch und merge ihn anschließend mit dem Master Branch.
- Commite viel, commite oft. Commite jeden Pups.
git-rmlöscht die Dateien wirklich endgültig von der Platte und entfernt sie nicht nur aus der Versionskontrolle.
git-guiundgitksehen schrecklich aus, sind aber mächtig und produktiv.- Giggle sieht gut aus, kommt aber nicht an
gitkran. projekt/.gitignoreist unnötig, 90% der Einträge gehören in die~/.gitignore- Subversion nervt immer mehr.
git-svnerst recht. - Github ruled!
- meld ist das beste diff-Tool des Planeten.
Zu guter letzt noch meine .gitconfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [gui] recentrepo = /home/martin/Workspace/foobar fontdiff = -family \"bitstream vera sans mono\" -size 8 -weight normal -slant roman -underline 0 -overstrike 0 [user] email = martin@mahner.org name = Martin Mahner [giggle] compact-mode = false main-window-maximized = false main-window-geometry = 1600x1123+0+52 [merge] tool = meld [core] excludesfile = /home/martin/.gitignore [color] branch = auto diff = auto interactive = auto status = auto |
und meine .gitignore:
1 2 3 4 5 6 7 8 9 10 | *.pyc *.pyo *~ *.swp *.orig .project .pydevproject .DS_Store MANIFEST dist |