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!
Model-Instances kopieren
Veröffentlich am 10.03.2009, 23:08
Ich war heute auf der Suche nach einer Möglichkeit, eine komplette Model-Instance inkl. aller Daten (normale Felder, ForeignKeys, Many-to-Many Feldern und Dateien) zu kopieren. Es sollte ein vollständiges Duplikat erzeugt werden.
Da ich nicht fündig geworden bin, möchte ich euch meine Implementierung nicht vorenthalten:
from django.db import models
from django.conf import settings
import os
import shutil
class CloneableModel(models.Model):
def clone(self):
initial = dict([(f.name, getattr(self, f.name))
for f in self._meta.fields if \
not isinstance(f, models.AutoField) \
and not isinstance(f, models.FileField) \
and not f in self._meta.parents.values()])
obj = self.__class__(**initial)
obj.save()
for field in [f.name for f in self._meta.many_to_many]:
oldfield = getattr(self, field)
setattr(obj, field, oldfield.all())
for field in [f.name for f in self._meta.fields if isinstance(f, models.FileField)]:
oldfield = getattr(self, field)
newfield = getattr(obj, field)
if oldfield:
newpath = newfield.field.storage.get_available_name(oldfield.name)
shutil.copy(
oldfield.path,
os.path.join(settings.MEDIA_ROOT, newpath)
)
setattr(obj, field, newpath)
obj.save()
return obj
class Meta:
abstract = True
Dieses Stück Code irgendwo in eurem Projekt abspeichern und die betroffenen Models von dieser Klasse statt von models.Model erben lassen. Dadurch erhalten euere Models eine zusätzliche Methode clone.
Diese Methode kopiert das gesamte Objekt mitsamt m2m Feldern und Dateien. Bei den Dateien ist zu beachten, dass jede Datei auch auf Filesystem Ebene kopiert wird, es wird also keine Referenz auf die Datei des alten Objekts angegeben.
Danke an Michael Elsdoerfer für die Grundidee.
Model-Instance Änderungen protokollieren
Veröffentlich am 18.02.2009, 17:45
Ich bin heute mal wieder auf ein scheinbar schwieriges Problem gestoßen.
Folgende Problemstellung ergibt sich:
Die Daten eines bestimmten Models sollen bei Änderung an einen anderen Dienstleister übertragen werden. Hierbei ist wichtig, dass nur geänderte Daten übertragen werden sollen.
- wie kann man mitbekommen, dass sich ein Objekt/Datensatz geändert hat?
- wie sollen diese "zum Export" gespeichert werden, ohne jedem Model ein weiteres Feld zuzuweisen?
- wie kann verhindert werden, dass Objekt mehrfach mehrfach übertragen wird?
Vorab erst einmal meine Lösung für die Problematik, die Erklärung folgt:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from datetime import datetime
class ExportQueue(models.Model):
content_type = models.ForeignKey(ContentType, verbose_name=_('Content type'))
object_id = models.PositiveIntegerField(_('Instance'))
instance = generic.GenericForeignKey()
date_added = models.DateTimeField(_('Date (created)'), default=datetime.now, blank=False)
date_exported = models.DateTimeField(_('Date (exported)'), blank=True, null=True)
is_exported = models.BooleanField(_('Is exported'), default=False)
def __unicode__(self):
return '%s (%s)' % (self.instance, self.date_added)
@staticmethod
def add_to_queue(instance):
try:
ctype = ContentType.objects.get_for_model(instance)
ExportQueue.objects.get(content_type__pk=ctype.id, object_id=instance.id, is_exported=False)
return False
except:
ExportQueue.objects.create(instance=instance)
return True
class Meta:
verbose_name = _('Export Queue')
verbose_name_plural = _('Export Queue')
class ExportableBaseModel(models.Model):
def __cmp__(self, other):
return cmp(';'.join(['%s' % self.__dict__[v] for v in self.__dict__]), ';'.join(['%s' % other.__dict__[v] for v in other.__dict__]))
def save(self, *args, **kwargs):
add_to_queue = False
if not self.pk:
add_to_queue = True
else:
old = type(self).objects.get(pk=self.pk)
if cmp(self, old):
add_to_queue = True
obj = super(ExportableBaseModel, self).save(*args, **kwargs)
if add_to_queue:
ExportQueue.add_to_queue(self)
return obj
class Meta:
abstract = True
Alle Models, die exportiert werden sollen, erben von ExportableBaseModel. Dadurch muss nicht in jedem betroffenen Model ein Feld export_needed (o.ä.) einfügt werden.
Das ExportableBaseModel überschreibt die normale save Methode von models.Model. In dieser neuen save Methode wird dann das aktuelle Objekt (aus der Datenbank) mit dem geänderten und noch nicht gespeicherten neuen Objekt verglichen.
Dieser Vergleich basiert auf zwei Strings, welche aus dem zusammengeführten *dict* Feld der beiden Objekte besteht. Wenn dieser Vergleich nun eine Änderung feststellt, wird die add_to_queue Methode des ExportQueue Models aufgerufen.
Die add_to_queue Methode prüft vor dem Anlegen eines neuen Eintrags, ob bereits das gleiche Objekt in der ExportQueue angelegt ist (und berücksichtigt dabei auch das is_exported Feld).
Das in meinem Code-Beispiel enthaltene Model ExportQueue hat ein Feld is_exported, welches nach dem Export auf True gesetzt wird. So kann nachvollzogen werden ob das Objekt bereits exportiert wurde. Zusätzlich gibt es noch ein date_exported Feld, welches nachvollziehbar macht, wann der Datensatz exportiert wurde. (das Setzen dieser Felder ist im Beispiel nicht implementiert!)
Diese Lösung ist sicher noch optimierungswürdig, aber funktioniert grundsätzlich.