ReadOnlyWidget für Django
Veröffentlich am 11.05.2009, 20:09
Es wird mal wieder Zeit, ein Stück Code zu veröffentlichen :-)
Nachdem ich auf der EuroDjangoCon vergangene Woche in Prag mehrfach nach einer Möglichkeit gefragt wurde, Felder - vorallem im Django Admin - read-only darzustellen, möchte ich meine Lösung dafür hier zur Verfügung stellen.
Das Widget ist einfach erweiterbar und unterstützt derzeit folgende Modelfelder: TextField, CharField, TagField, IntegerField, BooleanField, FileField, ImageField, ForeignKey, ManyToManyField, DateTimeField, DateField.
Um das Widget um eigene Datentypen zu erweitern, muss man das Widget subclassen und um eine Methode get_FELDNAME_value erweitern. Am besten schaut ihr euch einfach die anderen Felder an, der Code ist einfach und selbsterklärend.
Das ReadOnlyWidget:
from django import forms
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape
from django.contrib.admin.templatetags.admin_list import _boolean_icon
class ReadOnlyWidget(forms.HiddenInput):
def __init__(self, db_field, *args, **kwargs):
self.db_field = db_field
super(ReadOnlyWidget, self).__init__()
def render(self, *args, **kwargs):
field_name, value = args
field_type = self.db_field.__class__.__name__
field_value = super(ReadOnlyWidget, self).render(*args, **kwargs)
output = value
if hasattr(self, 'get_%s_value' % field_type.lower()):
try:
func = getattr(self, 'get_%s_value' % field_type.lower())
output = func(field_name, value)
except Exception,e:
output = e
else:
raise Exception('%s is not supported by ReadOnlyWidget.' % field_type)
return self.render_output(field_name, field_value, output)
def render_output(self, field_name, field_value, output):
return mark_safe('%s %s' % (output, field_value))
def get_textfield_value(self, field_name, value):
return '<p style="clear:both;">%s</p>' % value
def get_charfield_value(self, field_name, value):
if self.db_field.choices:
for choice in self.field.choices:
if value == choice[0]:
return conditional_escape(force_unicode(choice[1]))
else:
return escape(value)
def get_tagfield_value(self, field_name, value):
return escape(value)
def get_integerfield_value(self, field_name, value):
return '%d' % value
def get_filefield_value(self, field_name, value):
if value:
return '%s <a target="_blank" href="%s">%s</a>' % ('Currently:', value.url, value.name)
else:
return ''
def get_imagefield_value(self, field_name, value):
return self.get_filefield_value(field_name, value)
def get_foreignkey_value(self, field_name, value):
try:
obj = self.db_field.rel.to.objects.get(**{self.db_field.rel.get_related_field().name: value})
return '<strong>%s</strong>' % unicode(obj)
except:
return ''
def get_manytomanyfield_value(self, field_name, value):
output = ['<ul class="m2m_list_%s">' % field_name,]
for id in value:
output.append('<li>%s</li>' % unicode(self.db_field.rel.to.objects.get(pk=id)))
output.append('</ul>')
print self.help
return ''.join(output)
def get_datetimefield_value(self, field_name, value):
if value:
return value.strftime('%x %X')
else:
return ''
def get_datefield_value(self, field_name, value):
if value:
return value.strftime('%x')
else:
return ''
Das Widget speichert ihr am besten in einer widgets.py an einem sinnvollen Platz in eurem Projekt.
Die Einbindung des Widgets gestaltet sich auch sehr einfach. Hier ein Beispiel, um ein Model im Django Admin komplett read-only darzustellen (es muss ausschließlich die formfield_for_dbfield Methode überschrieben werden):
class TestAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(TestAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if field:
field.widget = ReadOnlyWidget(db_field=db_field)
return field
Natürlich könnt ihr das Widget auch in eurem Frontend verwenden, jedoch solltet ihr unbedingt die Methode get_booleanfield_value beachten, da diese eine Funktion aus dem Django Admin verwendet und in eurem Frontend aufgrund fehlender Mediadaten möglicherweise nicht funktioniert.
Update:
Ich habe dem Widget mal ein eigenes App gegönnt. Ihr könnt den aktuellen Code auf BitBucket auschecken oder per easy_install django_readonlywidget installieren.
Feedback und Anregungen zur Verbessserung sind ausdrücklich erwünscht.
Viel Spass!
AdminFileWidget mit Delete-Funktion
Veröffentlich am 06.11.2008, 10:00
Nachdem ich bereits längere Zeit nach einer Möglichkeit gesucht habe, den Inhalt von File-Feldern über Newforms-Admin löschen zu können, habe ich nun selbst ein kleines Widget geschrieben, welches genau das erledigt.
Neugeschrieben? Manch einer wird sich fragen, warum ich nicht eines der existierenden Snippets (#1, #2, es gibt sicher noch weitere) von djangosnippets.org nutze. Ganz einfach: sie machen eine Änderung des Models notwendig und genau das will ich nicht. Ich möchte meine Models nicht aufgrund eines Admin-Features ändern müssen.
Ich beginne erstmal mit dem Code selbst, Erläuterungen folgen:
from django.utils.safestring import mark_safe
class AdminDeleteFileWidget(admin.widgets.AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
output.append(super(AdminDeleteFileWidget, self).render(name, value, attrs))
output.append('<br /><input type="checkbox" name="adfw_%s_delete" /> %s' % (name, 'Delete'))
return mark_safe(u''.join(output))
class DeleteModelAdmin(admin.ModelAdmin):
def response_change(self, request, obj):
from django.db.models.fields.files import FileField
for field in obj._meta.fields:
if isinstance(field, FileField):
if request.POST.has_key('adfw_%s_delete' % field.name):
try:
file = getattr(obj, field.name)
file.delete()
except:
pass
return super(DeleteModelAdmin, self).response_change(request, obj)
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(DeleteModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if field and isinstance(field.widget, admin.widgets.AdminFileWidget):
field.widget = AdminDeleteFileWidget()
return field
Der Aufbau ist relativ simpel. Ich habe die AdminFileWidget Klasse von Newforms-Admin beerbt und an die reguläre Ausgabe noch eine Checkbox zum Löschen angehängt. Der Name ist bewusst kompliziert gewählt, um zu vermeiden das ein normales Model-Feld überschrieben wird (adwf_feldname_delete).
Beim laden des Formulars wird für jedes Model-Feld die Methode formfield_for_dbfield aufgerufen. Diese Methode wird durch die DeleteModelAdmin Klasse überschrieben und prüft für jedes Feld, ob es sich um ein File-Feld handelt. Wenn ja, wird das Widget durch das AdminDeleteFileWidget ausgetauscht.
Beim Speichern des Formulars wird die Methode response_change aufgerufen. Hier prüft die Methode für jedes Model-Feld, ob es sich um ein File-Feld handelt und wenn dies zutrifft prüft die Methode weiter, ob das eine Datei hinterlegt ist. Ist das der Fall, wird das Vorhandensein der POST Variable adfw_feldname_delete abgefragt und bei erfolg die Datei gelöscht.
Um diese Funktionalität einsetzen zu können, müssen die Model-spezifischen Adminklassen von DeleteModelAdmin statt admin.ModelAdmin erben.
Tags: django, newforms-admin, widget