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!

Tags: admin, django, forms, models, widgets

 

Kommentare

Kommentar von Martin Geber (17.05.2009, 10:42)

Hallo,

vielen Dank für diesen Code. Habe einige Anmerkungen, hoffe, dass das OK ist:

1. Du gehst relativ lax mit ``Exception`` um, ich würde ``raise AttributeError(`` anstelle von ``raise Exception(`` schreiben. UND: ``except Exception`` ist ein Freifahtschein, sollte man nur im äußerten Notfall anwenden.

2. Da du den Code als App hast, sollte er so wiederverwendbar wie möglich sein, dazugehört auch I18N-Unterstützung ("Currently").

3. Ich weiß nicht ob es sauberer ist, aber anstelle von::

if value:
return value.strftime('%x %X')
else:
return ''

Schreibe ich üblicherweise::

if value:
return value.strftime('%x %X')
return ''

Danke für den Code. Noch eine Frage (stehe auf dem Schlauch), ich möchte diesen Code *nur* für Update-Operationen anwenden, wie mache ich das? :)

Viele Grüße
Martin

 

Kommentar von Stephan Jäkel (17.05.2009, 11:44)

Hallo Martin,

1. das "except Exception" habe ich gewählt, da ich nicht weiß, welche Exceptions in den render-Methoden aufgerufen werden und ich alles abfangen möchte.
Bzgl. des "raise AttributeError" stimme ich zu, kann man verfeinern.
2. volle Zustimmung.
3. Einen expliziten else-Zweig zu haben, finde ich genauer bzw. definierter.

Zu deiner Frage, wie man das Widget nur in Change-Operationen/Edit verwendet, hier ein Beispiel (nicht getestet, sollte aber funktionieren): http://dpaste.de/WLjm/

 

Kommentar von Jonathan (13.02.2010, 00:20)

I am trying to get this to work in a regular ModelForm class, and obviously I am missing something. Can you give me a little more detail on how you use this in a non-admin application?

Here is an example of what I'm doing. http://dpaste.de/764c/

 

Kommentar von AscembomTeace (06.03.2010, 04:58)

Making money on the internet is easy in the underground world of <a href=http://www.www.blackhatmoneymaker.com>blackhat ebook</a>, Don’t feel silly if you have no clue about blackhat marketing. Blackhat marketing uses not-so-popular or little-understood methods to build an income online.

 

Kommentar von AscembomTeace (14.03.2010, 05:41)

Making money on the internet is easy in the underground world of <a href=http://www.www.blackhatmoneymaker.com>blackhat blog</a>, You are far from alone if you haven’t heard of it before. Blackhat marketing uses not-so-popular or little-understood methods to produce an income online.

 

 

Kommentar schreiben