Es lebt!
Veröffentlich am 11.02.2010, 21:04
Nach nunmehr knapp 9 Monaten Stille hier im Blog geht's wieder weiter.
Der Hauptgrund für die Ruhe war meine tägliche Arbeit. Leider konnte ich mich im letzten Dreivierteljahr kaum oder nur wenig mit Django befassen. Seit Februar hat sich das allerdings geändert. Endlich wieder Django :-)
Es hat sich in der deutschen Django Community viel getan, so gibt es mittlerweile einen Verein und die nächste DjangoCon wird in Berlin stattfinden. Weitere Informationen dazu gibt es hier.
Nach langer Zeit habe ich auch mal wieder einen Vortrag beim Berliner Django Stammtisch gehalten. Das Thema war "FeinCMS - eine Einführung". Die Folien dazu sind auch online verfügbar.
FeinCMS wird auch weiterhin ein wichtiges Thema für mich sein, da ein großer Teil meiner Arbeit mit dem System und dessen Erweiterung zutun hat.
Ich hoffe, ich verspreche nicht zu viel ;-) Wir werden sehen, ob ich ein wenig Content zusammenbekomme.
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!
Badword Filter für Kommentare
Veröffentlich am 11.03.2009, 12:56
Mich hat in den letzten Tagen eine heftige Spamwelle erwischt. Da ich die im django.contrib verfügbare Comments App verwende, gibt es neben dem honeypot Feld keinen Spamschutz.
Da mir Rechenaufgaben, Captchas und handische Moderation zu aufwändig sind, werde ich es mit einem Badword Filter versuchen.
Der Filter arbeitet sehr einfach. Über das comment_will_ne_posted Signal wird der Kommentartext auf Badwords überprüft und im Falle eines Treffers auf versteckt gesetzt. Dadurch wird der Kommentar nicht gelöscht und ich kann False-Positives wieder zum Leben erwecken.
models.py
from django.db import models
from django.utils.translation import ugettext as _
import re
class CommentBadword(models.Model):
word = models.CharField(_('Bad word'), max_length=255, blank=False)
is_regex = models.BooleanField(_('Is regex'), default=False)
def __unicode__(self):
return self.word
def validate(self, text):
if self.is_regex:
if re.compile(self.word).search(text):
return False
else:
if self.word in text:
return False
return True
class Meta:
verbose_name = _('Comment Badword')
verbose_name_plural = _('Comment Badwords')
forms.py
from django import forms
from django.utils.translation import ugettext as _
from .models import CommentBadword
import re
class CommentBadwordForm(forms.ModelForm):
def clean(self):
if self.cleaned_data['is_regex'] and not self._errors.get('word', None):
try:
test = re.compile(self.cleaned_data['word'])
except:
self._errors['word'] = forms.util.ErrorList([_('Invalid regex pattern')])
return self.cleaned_data
class Meta:
model = CommentBadword
admin.py
from django.contrib import admin
from .models CommentBadword
from .forms import CommentBadwordForm
class CommentBadwordAdmin(admin.ModelAdmin):
list_display = ('word', 'is_regex')
list_filter = ('is_regex',)
search_fields = ('word',)
form = CommentBadwordForm
admin.site.register(CommentBadword, CommentBadwordAdmin)
init.py oder auch woanders, hauptsache der Connect wird ausgeführt
from django.contrib.comments.models import Comment
from django.contrib.comments.signals import comment_will_be_posted
from .models import CommentBadword
def comment_badword_filter(sender, **kwargs):
instance = kwargs['comment']
for badword in CommentBadword.objects.all():
if not badword.validate(instance.comment):
instance.is_public = False
return True
comment_will_be_posted.connect(comment_badword_filter, sender=Comment, dispatch_uid='comment_badword_filter')
Ich denke, dass sich der Code zu großen Teilen selbst erklärt. Zwei Anmerkungen möchte ich jedoch loswerden.
Das CommenBadword Model kann sowohl mit einzelnen gesperrten Wörtern als auch mit regulären Ausdrücken umgehen. Eine entsprechende Validierung im Django Admin wird durchgeführt.
Theoretisch kann man diesen Badword Filter auch für andere Texte verwenden (Foren, etc.). Zur Validierung muss lediglich die Methode validate des CommentBadword Models mit dem zu prüfenden Text aufgerufen werden.Beispiel:
mybadword.validate(mytext)
.. gibt True zurück, wenn der Text keine Badwords enthält und somit freigegeben werden kann.
Viel Spass damit!
Tags: comments, django, moderation
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.
Umwandlung struct_time in datetime
Veröffentlich am 22.02.2009, 21:19
Ich habe heute mal wieder ein paar Zeilen für django-gitweb getippt. Dabei ist mir aufgefallen, dass GitPython für die Zeitstempel der Commits ein time.struct_time verwendet und nicht, wie erwartet, ein datetime-Objekt.
Nicht sehr angenehm, wenn man einige Tools von Django - wie zum Beispiel timesince - nutzen möchte.
Nachdem ich ein wenig herumprobiert habe, fand ich doch einen sehr einfach Weg, diese time.struct_time in datetime-Objekte umzuwandeln.
In [1]: from datetime import datetime
In [2]: import time
In [3]: ts = time.gmtime()
In [4]: ts
Out[4]: (2009, 2, 22, 19, 55, 33, 6, 53, 0)
In [5]: datetime(*ts[0:6])
Out[5]: datetime.datetime(2009, 2, 22, 19, 55, 33)
Die Funktion time.gmtime gibt ein time.struct_time zurück. Da für datetime-Objekte nur Jahr, Monat, Tag sowie Stunden, Minuten und Sekunden notwendig sind, übergebe ich nur die ersten sechs Werte von ts.
Um mir die Angabe der Werte zu erleichtern, setze ich den notwendigen Teil von time.struct_time als Argumentenliste (*), statt jedes Argument einzeln (datetime(ts[0], ts[1],...) zu übergeben.