Alle Beiträge in grober Übersicht
- 09.04.09: Überschreiben hochgeladener Dateien
-
Auszug: Wird eine Datei für ein FileField oder ImageField hochgeladen und exisitiert bereits, überschreibt Django diese Datei nicht sondern hängt einen Tiefstrich an die Datei. Die ältere Datei bleibt ohne Verweis im Dateisystem bestehen. Überschreibst du also eine Datei `rechnung.pdf` drei mal, entsteht eine lustige Kaskade im Dateisystem. - rechnung.pdf - rechnung\_.pdf - rechnung\_\_.pdf - rechnung\_\_\_.pdf (Der Pfad zu dieser Datei wird im FileField gespeichert) Es ist nicht immer im Interesse, diese alten Dateien zu behalten. Sei es aus Platzmangel, Datenschutzgründen oder weil man einfach nur Ordnung auf seinem System halten will. Derzeit gibt es keinen direkten Weg, das überschreiben zu erzwingen. Es gibt schon seit längerer Zeit ein [Ticket][ticket] im Django-Repository aber es schaut nicht so aus, als ob es in Django 1.1 eingespielt wird. [ticket]: http://code.djangoproject.com/ticket/4339 ### Automatisches Übeschreiben mit einem Custom-Storage ### Hochgeladene Dateien werden von einem eigenen Modul, dem `FileSystemStorage` verarbeitet. Es öffnet und speichert die Dateien und prüft, ob der Dateiname schon exisitiert und hängt ggf. einen Tiefstrich an die Datei. Das passiert in der Methode `get_available_name`. Genau diese ist der Ansatzpunkt um das Überschreiben zu erzwingen. Hier wird ein eigenes Storage erstellt aber vor dem Speichern der Datei, wird (wenn sie existiert) die alte Datei gelöscht: #!python from django.db import models from django.core.files.storage import FileSystemStorage class OverwriteFileSystemStorage(FileSystemStorage): def get_available_name(self, name): if self.exists(name): self.delete(name) return name class Invoice(models.Model): title = models.CharField(max_length=120) invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage()) def __unicode__(self): return self.title Das Custom-Storage wird dem `FileField` oder dem `ImageField` als Attribut `storage` übergeben. Soll dieses Schema für *alle* Uploads gelten, ändere in der `settings.py` den Wert `DEFAULT_FILE_STORAGE` Standardmässig wird das oben genannte `FileSystemStorage` verwendet. #!python # Original DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' # Immer alle Dateien überschreiben DEFAULT_FILE_STORAGE = 'path.to.OverwriteFileSystemStorage' ### Den originalen Dateinamen zusätzlich speichern ### Dass Dateien nicht grundsätzlich überschrieben werden ist ja grundsätzlich keine schlechte Idee. Schließlich hat man immer ein automatisches Backup aller Änderungen. Lassen wir das Schema so wie es ist aber speichern wir den originalen Dateinamen zusätzlich in einem eigenen Feld. So können wir dem User später den orignalen Dateinamen anzeigen, im Dateisystem bleiben aber alle überschriebenen Dateien erhalten. Ein upload-Callable ist ein schneller und sicherer Weg, dies zu erreichen. Mehr dazu hatte ich schon in [Ordnung im Medienordner][ordnung] geschrieben. [ordnung]: http://www.mahner.org/weblog/ordnung-im-medienordner-dynamische-upload-pfade/ #!python from django.db import models class Invoice(models.Model): def upload_invoice(instance, filename): instance.invoice_orig_filename = filename return filename title = models.CharField(max_length=120) invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage()) invoice_orig_filename = models.CharField(max_length=100, blank=True) def __unicode__(self): return self.title ### Dateien löschen ### Der Vollständigkeit halber: Von Haus aus ist es im Admin noch nicht möglich, hochgeladene Dateien wieder zu löschen. [Stephan Jäkel][sjaeckel] hat ein [Admin-Widget][admin-widget] konstruiert, dass Dateien auf Klick wieder löscht. [sjaeckel]: http://www.sjaekel.com/ [admin-widget]: http://www.sjaekel.com/blog/2008/11/6/22-adminfilewidget-mit-delete-funktion/ …
Weiter lesen: Überschreiben hochgeladener Dateien
- 08.04.09: Group By Funktionen in Querysets (Django 1.1+)
-
Auszug: In meinem aktuellen Projekt will ich die Anzahl neuer Einträge in einem Model pro Tag in einer Grafik darstellen, ich habe also ein Problem wie: > Gib mir eine Liste aller Tage an denen Beiträge hinzugefügt wurden und wie viele Beiträge waren es an diesem Tag. Ein SQL-Query dafür ist einfach: #!sql SELECT count(id) as counter, created FROM entry_table GROUP BY created Nun schauen wir mal, wie sich das mit Django 1.1 Bordmitteln lösen lässt. Wir erstellen ein Django-Model: #!python from django.db import models class Entry(models.Model): created = models.DateField() title = models.CharField(max_length=120) Und füttern es gleich mit ein paar Testdaten: #!pycon >>> Entry.objects.create(created='2009-03-25', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel') Während man in Django 1.0 noch rohes SQL walten lassen musste, wurden Django 1.1 (aktuell noch *django-trunk*) einige [Aggreagations-Funktionen][aggregation] hinzugefügt, um übliche Aufgaben wie Count, Average, Max und Min etc. direkt im Queryset durchführen zu können. Für unseren Fall brauchen wir die [Count][aggregation-count]-Funktion. [aggregation]: http://docs.djangoproject.com/en/dev/ref/models/querysets/#id8 [aggregation-count]: http://docs.djangoproject.com/en/dev/ref/models/querysets/#id9 #!pycon >>> from django.db.models import Count >>> Entry.objects.values('created').annotate(counter=Count('id')) [{'counter': 2, 'created': datetime.date(2009, 3, 24)}, {'counter': 1, 'created': datetime.date(2009, 3, 25)}] `values('created')` triggert hierbei das "GROUP BY created" und `Count('id')` ist das Pendant zur SQL-Funktion `COUNT(id)`. `annotate` *fügt* diese Daten dem SELECT hinzu. Super einfach, oder? :) ### Umgang mit DateTime Feldern ### Nun ist es aber in der Regel so, dass man einem Artikel kein `DateField` sondern ein `DateTimeField` zuweist, also die Zeit mitspeichert: #!python class Entry(models.Model): created = models.DateTimeField() title = models.CharField(max_length=120) Füttern wir das Model wieder mit ein paar Testdaten: #!pycon >>> Entry.objects.create(created='2009-03-25 12:30:00', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24 10:25:00', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24 8:56:00', title='Irgendein Titel') Und führen unseren obigen Query aus: #!pycon >>> Entry.objects.values('created').annotate(counter=Count('id')) [{'counter': 1, 'created': datetime.datetime(2009, 3, 24, 8, 56)}, {'counter': 1, 'created': datetime.datetime(2009, 3, 24, 10, 25)}, {'counter': 1, 'created': datetime.datetime(2009, 3, 25, 12, 30)}] Man sieht, das Ergebnis ist unbrauchbar. Logisch, denn es wird nach Datum *und* Zeit gruppiert. Wir brauchen also nur das Datum aus dem DateTime-Objekt. In SQL gibt es dafür die Funktion `DATE(datetime-Feld)` die das Datum extrahiert: #!sql SELECT COUNT(id) as counter, DATE(created) as day FROM entry_table GROUP BY day Dummerweise liefert Django keinen Filter mit, um eine solche Aufgabe zu lösen. Es gibt zwar die [dates][methdates] Methode, um Zeitperioden aus einem Queryset zu extrahieren, diese liefert aber nur die DateTime-Objekte zurück, kein komplettes Queryset, dass für `annotate` notwendig ist. Es muss also doch (etwas) rohes SQL her. Dafür ist die [extra][methextra] Methode gedacht. Mit ihr kann man rohes SQL in den Query *einsetzen*. Folgendes Beispiel resultiert gleich: #!text Queryset: Entry.objects.extra(select={'day': 'DATE(`created`)'}) SQL: SELECT *, DATE(`created`) AS day FROM entry_table [methdates]: http://docs.djangoproject.com/en/dev/ref/models/querysets/#dates-field-kind-order-asc [methextra]: http://docs.djangoproject.com/en/dev/ref/models/querysets/#extra-select-none-where-none-params-none-tables-none-order-by-none-select-params-none Zusammengefügt schaut unser Queryset jetzt so aus: #!pycon >>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'}) .values('day').annotate(counter=Count('id')) >>> entries [{'counter': 2, 'day': u'2009-03-24'}, {'counter': 1, 'day': u'2009-03-25'}] Die Tage sind hierbei aber ein unicode-String, ein DateTime-Objekt wäre schöner. Mappen wir den Queryset noch einmal: #!pycon >>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'}) .values('day').annotate(counter=Count('id')) >>> entries = [{'counter': i['counter'], 'day': datetime.strptime(i['day'], '%Y-%m-%d')} for i in entries] >>> entries [{'counter': 2, 'day': datetime.datetime(2009, 3, 24, 0, 0)}, {'counter': 1, 'day': datetime.datetime(2009, 3, 25, 0, 0)}] Da sind sie, die Tage an denen Beiträge hinzugefügt wurden inkl. deren Anzahl. :-) War diesmal vielleicht (noch) verworrener als sonst, wer Fragen dazu hat kann mir wie immer mailen oder in die Kommentare schreiben. :-) …
Weiter lesen: Group By Funktionen in Querysets (Django 1.1+)
- 21.03.09: Automatisch lokalisierte Zeitformatierungen
-
Auszug: Ein nur knapp [dokumentiertes][i18n] Feature ist die Lokalisierung von String *innerhalb* von Templatetags und Filtern. Dort reicht es, den betreffenden String in einen gettext-Shortcut zu setzen: #!django {{ _("hello world")|upper }} Dieser Token würde als `HELLO WORLD` ausgegeben und natürlich auch mit den betreffenden *.po* Dateien lokalisierbar sein. Richtig sinnvoll wird es beim `date` Filter. Bisher hast du vielleicht das Datum fest vorgegeben: #!django {{ entry.published|date:"d.m.Y H:M:S" }} Funktioniert im Deutschen ganz gut, aber da ja bekannterweise jede Sprache das Datum und die Zeit irgendwie anders formatiert, ist es eine gute Idee, die Formatierung auch lokalisierbar anzubieten: #!django {{ entry.published|date:_("DATETIME_FORMAT") }} Schaut ein wenig komisch aus, funktioniert aber. :-) Clevererweise kennt Django bereits den i18n-String `DATETIME_FORMAT` und liefert für jede mitgelieferte Sprache die entsprechende Formatierung in den gettext-Katalogen mit. Je nach Locale-Einstellung ist die Ausgabe landestypisch: #!text de-de: 21. März 2009, 20:24 en-us: March 21, 2009, 8:24 p.m. pt-br: 21 de Março de 2009 às 20:24 Neben `DATETIME_FORMAT` werden auch gleich `DATE_FORMAT`, `TIME_FORMAT`, `YEAR_MONTH_FORMAT` und `MONTH_DAY_FORMAT` mitgeliefert. :-) [i18n]: http://docs.djangoproject.com/en/dev/topics/i18n/?from=olddocs#in-template-code …
Weiter lesen: Automatisch lokalisierte Zeitformatierungen
- 06.03.09: Sichere Settings in der ServerError Site
-
Auszug: Manchmal lässt es sich nicht vermeiden, auf einem Live-System Djangos Fehler (Exception) Seite, den 500-View anzeigen zu lassen. Sie enthält den Traceback sowie den Umgebungsvariablen und allen Settings, Dinge die ein *normaler* Besucher eigentlich nicht sehen soll. In den Settings sind schon einige, sicherheitstechnisch riskante Felder gesternt, `DB_PASSWORD` zum Beispiel. Fast jeder definiert aber eigene Settings, wie ich hier z.B. den Wert `TWITTER_USERNAME` mit meinem Twitter-Namen. Dieses Setting ist jetzt zwar nicht hochriskant aber es soll nur als Beispiel dienen. Andere, geheimere Settings wären z.B. dein Flickr-API-Key. Jedenfalls, der zufällige Besucher auf so einer 500er Seite soll diesen Wert **niemals** sehen. Eine Möglichkeit ist, den Namen *nicht* komplett in Großbuchstaben schreiben, `twitter_username = 'bartTC'` und schon wird er nicht mehr in dieser Liste auftauchen. Das funktioniert zwar wunderbar ... schaut aber komisch aus. Definitionen sollten in Großbuchstaben definiert werden. (Gibts ein PIP dazu?) Eine bessere Möglichkeit ist es, dem Namen der Funktion eines der Wörter `SECRET`, `PASSWORD` oder `PROFANITIES_LIST` hinzuzufügen. Ein Wert wie `TWITTER_SECRET_USERNAME` wird durch das vorhandene `SECRET` nämlich ausgesternt. :-) ### Weiter lesen ### - Eric Holscher hat einen Beitrag geschrieben, der die 500 und 404 Seiten nur Superusern oder Usern mit der IP in `settings.INTERNAL_IPS`, anzeigt: [Debugging Django in Production Environments](http://ericholscher.com/blog/2008/nov/15/debugging-django-production-environments/) …
Weiter lesen: Sichere Settings in der ServerError Site
- 03.03.09: Zugemüllt
-
Auszug: Was mach ich am Wochenende? - Ein Haus bauen. - Ein Buch schreiben. - Meinen Schreibtisch aufräumen. Dürfte alles etwa gleich lang dauern. :-( …
Weiter lesen: Zugemüllt
- 25.02.09: Sprachabhängige Template-Imports
-
Auszug: Zuerst die Erklärung, der vollständige Code folgt am Ende. Um Internationalisierung (i18n) bzw. Übersetzung in Django-Templates zu bringen, gibt es verschiedene Möglichkeiten, allen voran die Möglichkeit, Strings mittels `trans` oder `blocktrans` zu markieren und in den entsprechenden gettext-Dateien zu übersetzen. Das funktioniert gut und hat sich bewährt, solange es sich um Strings handelt. Möchte man eine komplette Seite übersetzen, inkl. Bildern und HTML-Code, wird es schwieriger, mal abgesehen davon, dass ich HTML in gettext-Strings einfach häßlich finde. Eine Möglichkeit ist es, `ifequal` mit dem aktuellen Sprachcode des Users (`LANGUAGE_CODE`) zu vergleichen: #!django {% ifequal LANGUAGE_CODE "de" %} Hier ganz viel deutscher Text. {% else %} Here is a lot of english text, the default language. {% endif %} Wird der Inhalt zu lang, kann man die Texte jeweils in eigene Templates auslagern und mittels `include` importieren: #!django {% ifequal LANGUAGE_CODE "de" %} {% include "vieltext_deutsch.html" %} {% else %} {% include "vieltext_english.html" %} {% endif %} Hier setzt der unten gezeigte Templatetag an. Er arbeitet prinzipiell wie der Standard-Include-Tag aber importiert automatisch das Template mit der jeweiligen Sprache. Der Templatename muss dabei das Format `.` als Dateinamen besitzen. Angenommen unser User ist mit deutscher Spracheinstellung unterwegs und sein `LANGUAGE_CODE` wäre `de`, der Aufruf: #!django {% langinclude "meintext.html" %} würde hier zuerst das Template `meintext.html.de` suchen und importierten. Schlägt dies fehl, importiert es wie gewohnt das Template `meintext.html`. Hier nun der Templatetag: #!python from django.template import Library, Node from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable from django.template.loader_tags import IncludeNode from django.template.loader import get_template from django.conf import settings register = Library() class ConstantLanguageIncludeNode(Node): def __init__(self, template_path): self.template_path = template_path def render(self, context): try: t = get_template('%s.%s' % (self.template_path, context['LANGUAGE_CODE'])) except TemplateDoesNotExist, KeyError: t = get_template(self.template_path) except: if settings.TEMPLATE_DEBUG: raise return '' return t.render(context) def do_language_include(parser, token): """ Looks up for a template based on the template-name plus the current users language code. Loads the template and renders it with the current context. Example:: {% langinclude "foo/some_include.html" %} Based on the users LANGUAGE_CODE, assumed we have 'de', it tries to render the template 'foo/some_include.html.de'. If that doesn't exists, it renders the template 'foo/some_include.html'. This is the default behavior of the include-Tag. Basically this is a shortcut for the following code, just with a fallback for the default template:: {% ifequal LANGUAGE_CODE "de" %} {% include "foo/some_include.html.de" %} {% else %} {% include "foo/some_include.html" %} {% endifequal %} """ bits = token.contents.split() if len(bits) != 2: raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] path = bits[1] if path[0] in ('"', "'") and path[-1] == path[0]: return ConstantLanguageIncludeNode(path[1:-1]) return IncludeNode(bits[1]) register.tag('langinclude', do_language_include) Der Quellcode auf [djangosnippets.org](http://www.djangosnippets.org/snippets/1341/). …
Weiter lesen: Sprachabhängige Template-Imports
- 19.02.09: Tägliche Postfix-Helfer
-
Auszug: Hier ist ein kurzer Überblick über Tools, die einem Postfix-Postmaster das tägliche Leben erleichtern. Alle Tools sind auf Debian und Ubuntu-Systemen in den Standard-Paketquellen enthalten. ### pflogsumm - Tägliche Zusammenfassung der Logfileaktivität ### *pflogsumm* erstellt eine detaillierte Übersicht über die Logfileaktivität des Tages oder des Vortages. Dazu gehört die Zusammenfassung aller Mails (ausgehend, eingehend, rejected etc.), deren Gründe, die Warnungen, Fehler und Panikattacken von Postfix und noch vieles mehr. Der tägliche pflogsumm-Bericht ist Pflicht für jeden, der einen Überblick über seine Mailserveraktivität haben möchte! #!text Postfix Log Zusammenfassung fuer Feb 18 Zusammenfassung --------------- Nachrichten 3168 empfangen 1709 zugestellt 24 weitergeleitet 23 zurueckgewiesen (83 Zurueckweisungen) 113 zurueckgeworfen 36233 abgelehnt (95%) 0 Ablehnungswarnungen 0 wartend 0 verworfen (0%) 440838k bytes empfangen 475401k bytes zugestellt 770 Sender 553 sendende hosts/domains 389 Empfaenger 183 empfangende hosts/domains ... [5000 weitere Statistikzeilen] Am besten installiert man sich einen Crontab, der einem jeden morgen die Statistiken des Vortages zumailt. Hier wird morgens um 06:00 der Bericht des Vortages erstellt: #!text 0 6 * * * /usr/sbin/pflogsumm -d yesterday /var/log/mail.info /var/log/mail.info.0 | mail -s "Mailstatistiken" postmaster@localhost - Homepage: [jimsun.linxnet.com/postfix_contrib.html](http://jimsun.linxnet.com/postfix_contrib.html) - Deutsche Version von Patrick Koetter: [postfix.state-of-mind.de/patrick.koetter/pflogsumm/](http://postfix.state-of-mind.de/patrick.koetter/pflogsumm/) - Installation: `apt-get install pflogsumm` ### Mailgraph - Mailstatistiken in Bunt ### *Mailgraph* ist ein Dämon der permanent das mail.log überwacht und Statistiken über empfangene und versandte Mails in einer [RoundRobin-Datenbank](http://oss.oetiker.ch/rrdtool/) speichert. Mit dabei ist das Perlscript *mailgraph.cgi* das aus diesen Daten wunderschöne Grafiken erstellt: Das Script befindet sich von Haus aus in `/var/lib/cgi-bin/mailgraph.cgi`. Eventuell musst du es erst bearbeiten und die Pfade zur RRD-Datenbank (`/var/lib/mailgraph/`) anpassen. Prüft nach der Installation auch noch einmal den Pfad zum Logfile; in der `/etc/default/mailgraph` lautet der Pfad für ein Standard-Setup `MAIL_LOG=/var/log/mail.info`. Ob und inwiefern das sinnvoll ist, mag jeder für sich selbst entscheiden. Ich nutze es als schnellen Blick um mich zu vergewissern, dass mit dem Server alles rund läuft. - Homepage: [mailgraph.schweikert.ch](http://mailgraph.schweikert.ch/) - Installation: `apt-get install mailgraph; /etc/init.d/mailgraph start` ### qshape - Queue-Statistiken ### Mit Postfix wird das kleine Tool *qshape* mitgeliefert, bei Debian-Installationen ist es unter `/usr/sbin/qshape` zu finden. qshape zeigt eine Summierung der Anzahl der E-Mails in der Queue. Am interessantesten ist wohl die deferred-Queue: # qshape deferred T 5 10 20 40 80 160 320 640 1280 1280+ TOTAL 59 0 0 0 0 1 0 56 2 0 0 example.com 56 0 0 0 0 0 0 56 0 0 0 h.cx 1 0 0 0 0 1 0 0 0 0 0 surfeu.de 1 0 0 0 0 0 0 0 1 0 0 couttsbath.com 1 0 0 0 0 0 0 0 1 0 0 So erhält man schnell einen Überblick, was auf dem Server hängt. ### pfqueue - Frontend für die Queues ### Eine alternative Oberfläche für qshape ist *pfqueue*. Dort wechselt man mit den Tasten 1, 2, 3 und 4 zwischen den Queues (deferred, active, incoming, hold). Mails kann man direkt mit *d* löschen, mit *r* requeuen und vieles mehr. pfqueue ist vom Handling her ungewöhnlich aber sehr mächtig. Am besten man druckt sich vor der Benutzung die Manpage aus. - Homepage: [pfqueue.sourceforge.net](http://pfqueue.sourceforge.net/) - Installation: `apt-get install pfqueue` …
Weiter lesen: Tägliche Postfix-Helfer
- 19.02.09: Variable Spamchecks mit Postfix trotz content_filter
-
Auszug: In den letzten Tagen hatte ich Gelegenheit meine Postfix-Konfiguration neu aufzubauen. Bisher hatte ich amavisd-new als Frontend-Filter für Spamassassin benutzt, in Ubuntu 8.04 funktioniert die Installation allerdings nicht mehr: #!text Starting amavisd: ERROR: MISSING REQUIRED BASIC MODULES: Compress::Zlib BEGIN failed--compilation aborted at /usr/sbin/amavisd-new line 171. (failed). invoke-rc.d: initscript amavis, action "start" failed. WARNING: Starting amavisd-new failed. Please check your configuration. Das CPAN-Modul respektive `libcompress-zlip-perl` ist installiert, vielleicht weis ein Leser ja Rat. Auf jeden Fall muss eine Alternative her. Im Spamassassin-Wiki ist [eine Methode beschrieben](http://wiki.apache.org/spamassassin/IntegratedSpamdInPostfix), wie man Spamassassin als globalen `content_filter` in Postfix einbindet. Prinzipiell ganz gut, allerdings werden so *alle* Mails gefiltert, die Postfix passieren. Ich möchte aber einige User und auch ganze Domains ausschließen. ### Installation ### Zu allererst muss sichergestellt sein, dass Spamassassin auch installiert ist, auf Debian-Systemen reicht ein `apt-get install spamassassin`. Starte den Dämon mittels `/etc/init.d/spamassassin start`. In der `/etc/postfix/master.conf` wird ein neuer Transport `spamcheck` hinzugefügt: #!text spamcheck unix - n n - - pipe user=nobody argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient} In die `/etc/postfix/main.conf` werden zwei Änderungen vorgenommen. Das Recipient-Limit für den genannten Spamassassin-Transport auf 1 setzen, damit jeweils nur eine E-Mail durchgereicht wird. Und als vorletzten Parameter -- vor dem abschließenden `permit` -- wird ein access_check auf die Datei `/etc/postfix/access_spamassassin` hinzugefügt. #!text spamcheck_destination_recipient_limit = 1 smtpd_recipient_restrictions = reject_non_fqdn_recipient, reject_non_fqdn_sender, permit_mynetworks, # ... RBL Listen, etc reject_unauth_destination, # Hier werden die Empfänger geprüft check_recipient_access hash:/etc/postfix/access_spamassassin, permit Zu guter letzt lege die Datei `/etc/postfix/access_spamassassin` an. Ihr ihr wird penibel definiert, welche Empfänger letztendlich durch Spamassassin geprüft werden: #!text example.org FILTER spamcheck:dummy user@example.com OK example.com FILTER spamcheck:dummy Der Aufbau ist einfach, an erster Stelle steht die Empfängeradresse oder Domain gefolgt von einem Leerzeichen oder Tab und dem Wort `FILTER spamcheck:dummy`. `spamcheck` ist unser oben definierter Transport, `dummy` gibt den nächsten Hop an, den die Mail nehmen soll. In unserem Fall ist es ja der localhost, in diesem Fall reicht einfach `dummy`. Wenn einzelne Benutzer ausgeschlossen werden sollen, setze ein OK dahinter, achte aber auf die Reihenfolge. Nicht vergessen, danach ein `postmap hash:/etc/postfix/access_spamassassin` auszuführen. ### Was passiert bei diesem Setup? ### Ein fremder Mailserver liefert die E-Mail ein, sofern sie alle Checks besteht passiert sie den Spamassassin-Recipient-Check und wird weitergeleitet zu `spamc` (rote Linie), einem Helferprogramm dass den Mailbody mit Spamassassin prüft. `spamc` schickt danach die Nachricht über das lokale `sendmail` Programm wieder an Postfix (blaue Linie). Da die Nachricht diesmal vom lokalen System kommt, durchläuft sie nicht alle Checks sondern wird schon bei der Prüfung auf lokale IPs `permit_mynetworks` an den Haupttransport (`virtual_transport`, `local_transport`, etc.), wie zum Beispiel Procmail, abgegeben. Steht die Zieladresse erst garnicht in der access-Datei, wird sie auch nicht mit Spamassassin geprüft und mittels des abschließenden `permit` an den originalen Haupttransport weitergeleitet (graue Linie). ### Nachteile ### Der große Nachteil bei diesem Setup ist, dass die Mail, bevor die `spamc` zugestellt wird, die Postfix-Queue verlässt. Es ist danach nicht mehr möglich, die Mail zu rejecten -- jedenfalls nicht ohne [Backscatter](http://de.wikipedia.org/wiki/Backscatter) zu erzeugen. Gerade das ist der große Vorteil von Amavis, wenn er als smtpd_proxy_filter eingebunden ist. So lassen sich dort alle oder bestimmte Mails ab einer gewissen Score rejecten. Da ich aber recht aggressiv Blacklisten einsetze ist die False-Positive Rate bei Spamassassin-erkannten Mails recht hoch, so dass ich damit leben kann, alle Mails letztendlich anzunehmen. In der nächsten Zukunft werde ich mir mal Gedanken machen (sprich Google fragen), ob man das oben gezeigte Schema nicht auch als smptd_proxy_filter verbauen kann. ### Meine Postfix-Konfiguration ### Konfigurationsdateien sind ja mindestens so spannend wie Screenshots. Da ich keine Sicherheitsbedenken habe sie zu veröffentlichen und sie dazu noch kommiertiert ist; hier ist meine komplette Postfix-main.conf. Vielleicht findet noch jemand Ideen darin. #!ini # -------------------------------------------------------------------- # Base Server Config # -------------------------------------------------------------------- myhostname = schwammkopf.mahner.org mydestination = localhost # myhostname steht in den virtual domains myorigin = /etc/mailname mynetworks = 127.0.0.0/8, 88.198.109.76, [::ffff:127.0.0.0]/104, [::1]/128 inet_interfaces = all smtpd_banner = $myhostname ESMTP - Der schwammige Mailserver append_dot_mydomain = no # Biff Tannen? Niemals! biff = no # foo+bar@example.com, brauch ich nicht recipient_delimiter = # Deutsche Bounce-Files bounce_template_file = /etc/postfix/bounce-templates/bounce.de-DE.cf # Systembenutzer alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases # -------------------------------------------------------------------- # SMTP Errorlimits # -------------------------------------------------------------------- # Maximale E-Mail Größe (TODO: auf 2kb setzen) 50MB message_size_limit = 52428800 # Wann soll eine Warnung erfolgen, wenn die Mail # nicht zugestellt werden kann? delay_warning_time = 1h # Wie lange soll die Mail in der Queue bleiben, # wenn keine Zustellung möglich ist? maximal_queue_lifetime = 1d bounce_queue_lifetime = 1d # Max und Min Zeit wenn der Connect fehlschläft (auch 4xx) # Greylisting ist typischerweise auf 5min eingestellt minimal_backoff_time = 330s maximal_backoff_time = 8000s # Wie lange nach dem HELO warten? smtp_helo_timeout = 60s # Wie viele Adressen in einer Nachricht maximal? # Mailman ftw! smtpd_recipient_limit = 16 # Wie viele Fehler bis wir müde werden und wie viele, # bis komplett blockiert wird smtpd_soft_error_limit = 3 smtpd_hard_error_limit = 12 smtpd_error_sleep_time = 1s # Ein nettes Hallo beim Connect bitte smtpd_helo_required = yes # Spammerzeit vergeuden smtpd_delay_reject = no disable_vrfy_command = yes # -------------------------------------------------------------------- # SMTP Checks & Restrictions # -------------------------------------------------------------------- smtpd_client_restrictions = smtpd_helo_restrictions = smtpd_sender_restrictions = smtpd_data_restrictions = smtpd_recipient_restrictions = check_client_access hash:/etc/postfix/access/client, check_helo_access hash:/etc/postfix/access/helo, check_sender_access hash:/etc/postfix/access/sender, check_recipient_access hash:/etc/postfix/access/recipient, # Blocken wenn Pipelining nicht angefragt wurde reject_unauth_pipelining, # Auf anständige Syntax prüfen reject_non_fqdn_recipient, reject_non_fqdn_sender, # DNS Checks reject_unknown_recipient_domain, reject_unknown_sender_domain, # Eigene Nutzer erlauben permit_mynetworks, permit_sasl_authenticated, # RBL Blacklisten reject_rbl_client zen.spamhaus.org, reject_rbl_client ix.dnsbl.manitu.net, reject_rbl_client bl.spamcop.net, reject_rbl_client blackhole.securitysage.com, # Greylisting check_policy_service inet:127.0.0.1:60000, # Relaying reject_unlisted_recipient, reject_unauth_destination, # Spamassassin Content Filter check_recipient_access hash:/etc/postfix/access/spamassassin, # Wer nicht in spamassassin steht, ist durch permit # -------------------------------------------------------------------- # TLS Empfang/Versand # -------------------------------------------------------------------- # smptd (Mailempfang) smtpd_tls_security_level=may smtpd_tls_cert_file=/etc/postfix/certs/cert.pem smtpd_tls_key_file=/etc/postfix/certs/key.pem smtpd_tls_CAfile = /etc/postfix/certs/CAcert.pem smtpd_tls_loglevel = 1 # Bei SSL-Verbindung diese im received-Header markieren smtpd_tls_received_header = yes # smtp (Mailversand) smtp_tls_security_level=may smtp_tls_cert_file=/etc/postfix/certs/cert.pem smtp_tls_key_file=/etc/postfix/certs/key.pem smtp_tls_CAfile = /etc/postfix/certs/CAcert.pem smpt_tls_loglevel = 1 # -------------------------------------------------------------------- # SASL (SMTP-Auth) # -------------------------------------------------------------------- smtpd_sasl_type=dovecot smtpd_sasl_path=private/auth smtpd_sasl_auth_enable=yes smtpd_sasl_security_options = noanonymous # -------------------------------------------------------------------- # Transport and Mapping # -------------------------------------------------------------------- # Dovecot transport virtual_transport = dovecot dovecot_destination_recipient_limit = 1 # Spamassassin Transport in den recipient restrictions spamassassin_destination_recipient_limit = 1 virtual_uid_maps=static:5000 virtual_gid_maps=static:5000 virtual_mailbox_domains = mysql:/etc/postfix/mysql/virtual-mailbox-domains.cf virtual_mailbox_maps = mysql:/etc/postfix/mysql/virtual-mailbox-maps.cf virtual_alias_maps = mysql:/etc/postfix/mysql/virtual-alias-maps.cf, mysql:/etc/postfix/mysql/email2email.cf …
Weiter lesen: Variable Spamchecks mit Postfix trotz content_filter
- 08.02.09: Plesk 8.x und 9.x: Keine Rechte zum Datenbank-Anlegen trotz Admin-Account
-
Auszug: In Plesk 8.x und auch noch in Plesk 9.0.x erscheint eine Fehlermeldung, wenn man eine neue Datenbank anlegen will: > Fehler: Parallels Plesk Panel kann keine Verbindung zum Datenbankserver herstellen, da kein gültiger Administrator-Benutzername und -Passwort angegeben wurden. Großartige Fehlermeldung. Natürlich sind die Rechte vorhanden und die Limits nicht ausgereizt. Der Fehler tritt nur auf, wenn man einen Webkit-Browser bzw. Safari benutzt. Also am einfachsten für diese Aufgabe zu Firefox wechseln. Mit Google-Chrome tritt der Fehler nicht auf, wohl weil es eine andere Javascript-Engine als Safari nutzt. Was wiederum bedeutet, dass Plesk wohl die DB-Zugangsdaten irgendwo im Javascript mitschleift und dort dann nicht findet. Alles sehr komisch. …
Weiter lesen: Plesk 8.x und 9.x: Keine Rechte zum Datenbank-Anlegen trotz Admin-Account
- 08.02.09: Ordnung im Medienordner: Dynamische Upload-Pfade
-
Auszug: Djangos ``FileField`` und das davon abgeleitete ``ImageField`` Feld enthalten ein [upload_to-Argument](http://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.FileField.upload_to) das angibt, wohin die Datei im statischen Medienordner gespeichert werden soll: #!python class Image(models.Model): image = models.ImageField(upload_to='images/') def __unicode__(self): return self.image.name def get_absolute_url(self): return self.image.url Ein hochgeladenes Bild wäre dann über die URL ``/media/images/filename.jpg`` zu erreichen. ``/media/`` ist der URL-Pfad zu den statischen Medien, der zuvor in der ``settings.py`` mit ``MEDIA_URL`` definiert wurde. ``upload_to`` muss aber kein festgelegter Pfad sein, es kann auch ein ``callable``, also eine Funktion oder jede Klasse die ein ``__call__`` Objekt bietet, verwerten. Wozu braucht man das? Immer dann, wenn der Pfad dynamisch sein soll. Im folgenden Beispiel soll eine hochgeladene Datei in mehreren Unterordnern abgelegt werden, wobei die Buchstaben der Unterordner die Anfangsbuchstaben der Datei sind, Huh, kompliziert? Gar nicht: #!python def bilder_in_unterordnern(instance, filename): if len(filename.rsplit('.')[0]) >= 3: # Dateiname ohne Extension muss # mindestens 3 Zeichen besitzen filename = "images/%s/%s/%s/%s" % (filename[0], filename[1], filename[2], filename) return filename class Image(models.Model): image = models.ImageField(upload_to=bilder_in_unterordnern) # ... Das ``upload_to`` Argument enthält den Namen einer Funktion, die den neuen Dateinamen (inklusive Pfad) zurück gibt. Beachte aber, dass das Funktionsargument hier keine Klammern besitzt, sonst würde sie nur einmal, beim Starten des Webservers, ausgeführt werden. Diese callable-Funktion besitzt immer zwei Argumente, die aktuelle Instanz des Modelobjekts und den originalen Dateinamen. Der Rückgabewert ist der neue Dateiname. Bilder die mit dem obigen Schema hochgeladen werden, würden dabei diese Pfade erhalten: - bild.jpg > /media/images/b/i/l/bild.jpg - foobar.jpg > /media/images/f/o/o/foobar.jpg - ... Schon ganz hübsch. :) Richtig sinnvoll wird es aber erst, wenn der Bildpfad Werte aus der selben oder gar einer anderen Model-Instanz enthalten soll. Angenommen die Bilder sind mit einem Artikel (von einem Weblog, natürlich) verknüpft und sollen als Dateipfad den Slug des verknüpften Artikels enthalten, so kann das Callable auf die aktuelle Model-Instanz und dessen Verknüpfungen zugreifen: #!python def bild_mit_articleslug(instance, filename): return 'images/%s/%s' % (instance.article.slug, filename) class Article(models.Model): title = models.CharField(max_length=100, blank=False) slug = models.SlugField() content = models.TextField() def __unicode__(self): return self.title class Image(models.Model): article = models.ForeignKey(Article) image = models.ImageField(upload_to=bild_mit_articleslug) Angenommen unser Artikel hat den Titel *"Mein neuer Blogbeitrag"* so wären die Dateinamen so: - bild.jpg > /media/images/mein-neuer-blogbeitrag/bild.jpg - foobar.jpg > /media/images/mein-neuer-blogbeitrag/foobar.jpg - ... So schafft man Ordnung im Dateisystem. :) ### Ausnahme Primärschlüssel ### Eine Ausnahme ist, wenn der Dateiname des Bildes den Primärschlüssel des Objekts enthalten soll. Da, bei einem Insert, der Primärschlüssel noch nicht gesetzt ist, existiert dieses Attribut noch nicht. Ein Umweg ist, erst das Bild hochzuladen, das Objekt zu speichern dann das Bild entsprechend umzubenennen und das Objekt wiederum zu speichern: #!python class Image(models.Model): image = models.ImageField(upload_to='tmp/') def save(self, *args, **kwargs): # Modelobjekt speichern und einen Primärschlüssel (self.pk) erhalten super(Image, self).save(*args, **kwargs) # Nur umbennenen wenn sich die Datei noch im tmp-Pfad befindet MEDIA_ROOT = os.path.abspath(settings.MEDIA_ROOT) if self.image.path.startswith(os.path.join(MEDIA_ROOT, 'tmp/')): # Neuen Dateinamen mit Pfad und Primärschlüssel generieren new_filename = '%s%s%s' % (os.path.join(MEDIA_ROOT, 'images/'), self.pk, os.path.splitext(self.image.name)[1]) # Letztendlich die Datei umbennenen und den neuen Pfad speichern old_filename = os.path.join(MEDIA_ROOT, self.image.name) os.rename(old_filename, new_filename) # Den neuen Dateinamen im Model setzen und noch einmal speichern self.image = new_filename super(Image, self).save(*args, **kwargs) Das ganze geht bestimmt noch hübscher und enthält einige Fallstricke, so muss der Ordner ``images/`` schon existieren. Aber seis drum, Bilder die so hochgeladen werden, würden als Dateinamen den Primärschlüssel des jeweiligen Modelobjekts erhalten: - bild.jpg > /media/images/1.jpg - foobar.jpg > /media/images/2.jpg - ... ### Dateinamen mit Datum und Zeit ### Der Vollständigkeit halber sei noch erwähnt, dass das ``upload_to`` Argument auch ohne Umwege [strftime-Argumente](http://docs.python.org/library/time.html#time.strftime) verwerten kann. Ein ``upload_to='/images/%Y/%m/'`` würde das Bild im Ordner ``/images/2009/02/bild.jpg`` speichern. …
Weiter lesen: Ordnung im Medienordner: Dynamische Upload-Pfade
- 09.01.09: Bitterkalt
-
Auszug: Überall in Deutschland frieren die Menschen. Überall? Nein, ein kleines Dorf am Rand der Scheibe wehrt sich gegen die Kälte. Hier regnet es sogar. :-( …
Weiter lesen: Bitterkalt
- 31.12.08: Spass mit Newforms-Admin - Ganz schnell Widgets zuweisen
-
Auszug: In den vergangenen *Spass mit Newforms-Admin* Beiträgen hab ich immer wieder gezeigt, wie man das Widget (die HTML-Ausgabe) eines Feldes beeinflusst und ändert. Als Beispiel nochmal ein simples Model, bei dem letztendlich der Titel in großer Schrift ausgegeben werden soll. #!python from django.db import models class Foobar(models.Model): title = models.CharField(max_length=100) text = models.TextField(blank=True) Also basteln wir uns -- wie gehabt -- eine ModelAdmin-Klasse und darin eine Methode *formfield_for_dbfield*. Diese durchläuft alle Felder und wenn sie auf den passenden Titel trifft, ändert sie das Widget dazu. #!python from django.contrib import admin from django import forms from django.utils.safestring import mark_safe from .models import Foobar class BigTitleWidget(forms.widgets.Input): def render(self, name, value, attrs=None): field = '' % (name, value) return mark_safe(field) class FoobarAdmin(admin.ModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): field = super(FoobarAdmin, self).formfield_for_dbfield(db_field, **kwargs) if db_field.name == "title": field.widget = BigTitleWidget() return field admin.site.register(Foobar, FoobarAdmin) Aber ist das nicht super hässlich? Irgendwann bin ich mal über einen Codeblock von [Jannis](http://www.jannisleidel.com/2008/11/autocomplete-widget-for-django-tagging-form-fields/) gestolpert und hab gesehen, dass die ModelAdmin-Klasse ein Attribut *form* besitzt, dass das automatisch generierte [ModelForm](http://docs.djangoproject.com/en/dev/topics/forms/modelforms/?from=olddocs) mit einem eigenen überschreiben kann. Hey, und das ist sogar [dokumentiert](http://docs.djangoproject.com/en/dev/ref/contrib/admin/#id1). #!python from django.contrib import admin from django import forms from django.utils.safestring import mark_safe from .models import Foobar class BigTitleWidget(forms.widgets.Input): def render(self, name, value, attrs=None): field = '' % (name, value) return mark_safe(field) class FoobarForm(forms.ModelForm): title = forms.CharField(widget=BigTitleWidget()) class FoobarAdmin(admin.ModelAdmin): form = FoobarForm admin.site.register(Foobar, FoobarAdmin) Das ist doch viel schöner! :-) Also nie wieder *formfield_for_dbfield*? Nicht ganz, denn innerhalb des ModelForms hat man keinen Zugriff auf die anderen Parameter des aktuellen Admin-Views. Will man dem Widget weitere Parameter übergeben (wie z.B. die aktuelle User-ID, [hier erklärt](http://www.mahner.org/weblog/spass-mit-newforms-admin-automatische-felder/), führt der Weg weiterhin über *formfield_for_dbfield*. …
Weiter lesen: Spass mit Newforms-Admin - Ganz schnell Widgets zuweisen
- 21.12.08: Unter der Lupe: Template-Context-Prozessoren
-
Auszug: Djangos Templates stehen zwei unterschiedliche Arten zur Verfügung, wie Variablen ins Template gelangen (Template-Filter und -Tags jetzt außen vorgelassen). Die lokalen Variablen, die pro View festgelegt werden und die Template-Context-Prozessoren (im folgenden *TCP* genannt), jeweils ein Dictionary mit Variablen, das **allen** Templates zur Verfügung steht. Einige TCPs sind in der Grundkonfiguration eines Django-Projekts schon mit dabei und bestimmt hast du sie schon einmal gesehen: #!python TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.auth', 'django.core.context_processors.i18n', 'django.core.context_processors.media', ) **Hinweis:** Wenn du ein neues Projekt mittels ``django-admin.py startproject foo`` erstellt hast, ist dieser Eintrag *nicht* in der ``settings.py`` vorhanden. Er steht aber in den Grundeinstellungen ``django.conf.global_settings``. Wenn du etwas ändern willst, packe den oberen Schnipsel ans Ende deiner ``settings.py``. Diese Tupel wird in der Einstellung ``settings.TEMPLATE_CONTEXT_PROCESSORS`` festgelegt. Am bekanntesten wird wohl der media-TCP sein, er stellt allen Templates die Variable ``MEDIA_URL`` zur Verfügung, ein Link zum Ordner mit den statischen Mediendateien. TCPs können natürlich auch selbst erstellt werden, die Syntax ist sehr einfach: #!python def mein_prozessor(request): return { 'VARIABLEN_NAME': 'wert', } Es ist eine einfache Funktion die als einziges Argument die aktuelle request-Variable nimmt und ein Dictionary zurückgibt. Eingebunden wird sie in die ``settings.py`` wie oben schon beschrieben: #!python TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.auth', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'mein_app.context_processors.mein_prozessor', ) Es ist eine gute Idee, die Datei die diese Funktion beinhaltet, auch 'context_processors.py' zu nennen. ### Nochmal im Vergleich ### Da sich mit Quellcode eine Sache am besten verstehen lässt, hier noch einmal ein einfacher View: #!python def startseite(request): return render_to_response( 'pfad/zum/template.html', {'headline': 'Meine Startseite', 'text': 'Lorem ipsum dolor bla.'}, RequestContext(request) ) Die hier benutzte ``render_to_response`` Funktion gibt das Template (erstes Argument) zurück mit den **lokalen Variablen** (zweites Argument) und stellt auch alle globalen TCPs bereit (drittes Argument). In einer flauschigen Grafik schaut das so aus: ### Was nutzt man wann? ### Wann man TCPs nutzt dürfte nun klar sein. Soll ein Wert **jedem** Template zur Verfügung stehen, baut man dafür einen TCP. Ein Beispiel ist auf dieser Seite meine Fußleiste, dort sind auf jeder Seite die neuesten Blogbeiträge und Bookmarks zu sehen. ### Der FAQ-Teil ### Nun bist du vielleicht auf diese Seite gelangt, weil man dich im Django-Channel hierher verwiesen hat. Jetzt kommen die häufigsten Fehler im Umgang mit Template-Context-Prozessoren (TCP). Eine typisches Problem dürfte zum Beispiel sein: > Ich kann in meinem Template die ``MEDIA_URL`` Variable nicht nutzen, sie gibt nur einen leeren String zurück. ### Mögliche Fehlerquellen ### **1. Groß/Kleinschreibung** Ganz simpel eigentlich. In Django-Templates werden Variablen zwischen Groß- und Kleinschreibung unterschieden. ``Media_URL`` ist also nicht das selbe wie ``MEDIA_URL``. **2. Du hast einen eigenen View geschrieben der render_to_response nutzt** Der häufigste Fehler ist, dass du ``render_to_response`` vergessen hast, als **drittes Argument** den Request-Context zur Verfügung zu stellen. Schau nochmal auf die Beispielfunktion von oben: #!python from django.template.context import RequestContext def startseite(request): return render_to_response( 'pfad/zum/template.html', {'headline': 'Meine Startseite', 'text': 'Lorem ipsum dolor bla.'}, RequestContext(request) ) ``RequestContext`` analysiert im Prinzip die Module aus ``settings.TEMPLATE_CONTEXT_PROCESSORS`` und stellt sie dem Template zur Verfügung. Djangos Generic-Views machen das automatisch! Warum ``render_to_repsonse`` nicht? Im Prinzip geht es darum, dass du die Möglichkeit *hast*, diese Variablen nicht dem Template zur Verfügung zu stellen. Klingt komisch, ist aber so. Oft ist dies der Fall, wenn Dritte Zugriff auf die Templates haben und nicht alle globalen Variablen wissen sollen. Das ist aber nicht DRY \*kreisch\*. Es gibt Mittel und Wege das semi-automatisch zu machen. Auf [dieser Seite](http://fi.am/entry/shortcutting-render_to_response/) sind einige Shortcuts erklärt. **3. Du willst auf den *request* TCP zugreifen** Bis vor Django 1.0 war der TCP ``django.core.context_processors.request`` noch Teil der Grundeinstellungen. Dieser wurde nun (aus Sicherheits- oder Performancegründen, ich weiß es nicht) entfernt. Viele 3rd-Party-Apps nutzen diesen TCP ohne zu dokumentieren, wie er eingebunden wird. Wenn du in deiner ``settings.py`` noch keine Definition für ``TEMPLATE_CONTEXT_PROCESSORS`` hast, packe diesen Schnipsel ans Ender der Datei: #!python TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.auth', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.request', ) Alternativ, wenn du schon eine Prozessoren-Liste hast, packe einfach ``django.core.context_processors.request`` dazu. Das wars, eine erschöpfende Antwort auf die #1 FAQ im Django IRC-Channel. :-) …
Weiter lesen: Unter der Lupe: Template-Context-Prozessoren
- 21.12.08: Request-unabhängige Locale-Einstellungen
-
Auszug: Manchmal ist es nötig, in einem Request/View verschiedene Locale-Einstellungen zu nutzen. Der häufigste Fall dürfte der Versand von E-Mails an verschiedene User sein, die jeweils die Nachricht in ihrer Sprache erhalten sollen. Man könnte einfacherweise für diese E-Mail für jede Sprache ein eigenes Template anlegen, aber wozu gibt es die gettext-Unterstütung. :) Djangos I18N-Kern bringt eine Funktion ``django.utils.translation.activate`` mit, mit der man zur Laufzeit die Locale-Einstellung ändern kann. Als Beispiel dient ein einfaches Template mit einem Translation-String. #!django {% spaceless %} {% load i18n %} {% trans "Thanks for using our site!" %} {% endspaceless %} Und eine Dummy-Funktion. Dabei wird über die Sprachen iteriert (normalerweise würde die jeweilige Spracheinstellung im Userprofil gespeichert sein) und das ``email.txt`` Template mit der jeweiligen Sprache im Terminal ausgegeben: #!python from django.template.loader import render_to_string from django.utils.translation import get_language, activate current_language = get_language() languages_to_render = ('en-us', 'de-de', 'pt-br', 'fr') for lang in languages_to_render: activate(lang) print render_to_string('email.txt') activate(current_language) Ausgabe: Thanks for using our site! Vielen Dank, dass Sie unsere Seite benutzen! Obrigado por usar nosso site! Merci d'utiliser notre site ! Einfach oder? Beachten sollte man noch, dass man den originalen Locale-Wert vor dem ändern speichert (``django.utils.translation.get_language`` liefert den aktuellen Wert) und am Ende wieder aktiviert. Sonst würde in diesem Fall der Rest des Views mit der französischen Locale weiterlaufen. …
Weiter lesen: Request-unabhängige Locale-Einstellungen
- 19.12.08: Hackfleischpizza
-
Auszug: Wohoo, endlich wieder Foodcontent. :) Heute: Wir basteln uns eine Hackfleischpizza. Ok, dass ist nun nicht soo was besonderes aber sei's drum, es ist lecker und schnell gemacht. Wir brauchen: - einen Pizzateig - 300g Rinderhack - eine Mozarellakugel (nennt man das so?) - eine Hand voll Mais - eine Zwiebel Das Hack wird in der Pfanne erst einmal angebraten, aber nicht ganz durch. Danach von der Pfanne erst einmal auf ein Küchentuch legen damit das überflüssige Öl abläuft, sonst schwimmt die Pizza später. Der Mozarella wird grob gewürfelt und die Zwiebel in Ringe geschnitten. Alles zusammen mit einer Handvoll Mais (hier aus der Dose) auf der Pizza verteilen und für ca. 20 Minuten bei 180°C in den Ofen. **Lääääcka!** Dazu gibt es einen weichen 1975'er Spätburgunder am Besten ein Bier nach Wahl. :-P ### Noch ein paar Gedanken dazu: ### Wer kann, macht sich den Pizzateig natürlich selber. Allerdings sagt mir mein Hobbykochherz, dass ein Hefeteig nicht so einfach herzustellen ist und schnell geht es schon gar nicht (der sollte über Nacht im Kühlschrank noch gehen). Ich für meinen Fall kaufe immer die fertigen Pizzateige inkl. Tomatensauce. Dabei sollte man nicht auf den Cent gucken. Die billigsten Fertigteige der Discounter schmecken *mir* nicht, sind dröge und die Sauce ist völlig überwürzt. Der Fertigteig von Knack&Back für knapp 2€ war jedenfalls lecker. Ich hab aber nur nach ihm gegriffen, weil mir das Männchen auf der Packung gefallen hat. Ja, ich bin der Traum des Einzelhandels. :D Und dann noch zum Belag. Leider ist es wohl eine deutsche Eigenart, eine Pizza mit hundert verschiedenen Belägen voll zu knallen, darüber noch eine 3cm dicke Käseschicht. Dabei ist weniger viel mehr, die Pizza ist viel leckerer je *weniger* Zutaten dabei sind. …
Weiter lesen: Hackfleischpizza
- 14.12.08: Eine bessere settings.py
-
Auszug: Bei einem frischen Django-Projekt ist so ziemlich die erste Änderung bei mir die ``settings.py``. Es gibt einige Sachen, die fehlen in der Grundkonfiguration **immer**. So z.B. der request-Context-Prozessor, Caching-Einstellungen oder die Mailserver-Einstellungen (ich betreibe lokal keinen SMTP). Daneben natürlich noch die Sachen, die ich in [Wiederverwendbare Django-Projekte](http://www.mahner.org/weblog/wiederverwendbare-django-projekte/) gepostet habe. Irgendwann habe ich mir mal eine generische ``settings.py`` erstellt in der ich auf Anhieb alle, für mich nötigen, Einstellungen vorfinde und diese mit sinnvollen Werten vorgeingestellt. Bei jedem neuen Projekt überschreibe ich die ``settings.py`` zuerst einmal mit dieser Variante. Vielleicht findet es ja jemand interessant. :-) #!python import os PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) PROJECT_NAME = os.path.split(PROJECT_ROOT)[-1] # ============================================================================== # debug settings # ============================================================================== DEBUG = True TEMPLATE_DEBUG = DEBUG INTERNAL_IPS = () if DEBUG: TEMPLATE_STRING_IF_INVALID = 'STRING_NOT_SET' # ============================================================================== # cache settings # ============================================================================== CACHE_BACKEND = 'locmem://' CACHE_MIDDLEWARE_KEY_PREFIX = '%s_' % PROJECT_NAME CACHE_MIDDLEWARE_SECONDS = 600 # ============================================================================== # email and error-notify settings # ============================================================================== ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS DEFAULT_FROM_EMAIL = 'from-mail@example.com' SERVER_EMAIL = 'error-notify@example.com' EMAIL_SUBJECT_PREFIX = '[%s] ' % PROJECT_NAME EMAIL_HOST = 'localhost' EMAIL_PORT = 25 EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_USE_TLS = False # ============================================================================== # auth settings # ============================================================================== LOGIN_URL = '/accounts/login/' LOGOUT_URL = '/accounts/logout/' LOGIN_REDIRECT_URL = '/' # ============================================================================== # database settings # ============================================================================== DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = os.path.join(PROJECT_ROOT, 'dev.db') DATABASE_USER = '' DATABASE_PASSWORD = '' DATABASE_HOST = '' DATABASE_PORT = '' # ============================================================================== # i18n and url settings # ============================================================================== TIME_ZONE = 'Europe/Berlin' LANGUAGE_CODE = 'de' LANGUAGES = (('en', 'English'), ('de', 'German')) USE_I18N = True SITE_ID = 1 MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'site_media') MEDIA_URL = '/media/' ADMIN_MEDIA_PREFIX = '/django_admin_media/' ROOT_URLCONF = '%s.urls' % PROJECT_NAME # ============================================================================== # application and middleware settings # ============================================================================== INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', 'django.contrib.humanize', ) MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.gzip.GZipMiddleware', 'django.middleware.common.CommonMiddleware', ) TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.auth', 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.request', ) TEMPLATE_DIRS = ( os.path.join(PROJECT_ROOT, 'templates'), ) TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', # 'django.template.loaders.eggs.load_template_source', ) # ============================================================================== # the secret key # ============================================================================== try: SECRET_KEY except NameError: SECRET_FILE = os.path.join(PROJECT_ROOT, 'secret.txt') try: SECRET_KEY = open(SECRET_FILE).read().strip() except IOError: try: from random import choice SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) secret = file(SECRET_FILE, 'w') secret.write(SECRET_KEY) secret.close() except IOError: Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE) # ============================================================================== # third party # ============================================================================== # ..third party app settings here # ============================================================================== # host specific settings # ============================================================================== try: from local_settings import * except ImportError: pass …
Weiter lesen: Eine bessere settings.py
- 04.12.08: Git-Branch im Bash-Prompt anzeigen
-
Auszug: Wer mit [Git](http://git.or.cz/) arbeitet und öfter mal Dateien im falschen Branch ändert -- wie ich ;) -- dem hilft vielleicht, den aktuellen Branch im Command-Prompt anzuzeigen. Das ganze schaut bei mir so aus: Der passende Eintrag für deine ``.profile`` oder ``.bashrc`` Datei lautet: #!bash export PS1='\[\033[01;32m\]\u\[\033[01;34m\] \w\[\033[31m\]$(__git_ps1 " (%s)")\[\033[01;34m\]$\[\033[00m\] ' Den Zauber macht die Kette ``$(__git_ps1 " (%s)")`` möglich. ``__git_ps1`` ist ein Befehl aus dem *contrib/completion* Verzeichnis der Git-Source-Distribution der den aktuellen Branch des Verzeichnisses ausgibt. Der wird aber hier unter Ubuntu mit dem Paket direkt mitinstalliert. Wer OSX nutzt und Git via Macports installiert hat, muss wahrscheinlich die Datei ``/opt/local/etc/bashcompletion.d/git`` sourcen. [Danke Horst für die Info](http://zerokspot.com/weblog/2008/12/04/git-branches-and-ps1/)! Und wer noch mehr wissen will, findet bei [asemanfar.com](http://asemanfar.com/Current-Git-Branch-in-Bash-Prompt) weitere Promt-Varianten. …
Weiter lesen: Git-Branch im Bash-Prompt anzeigen
- 04.11.08: Durchwahl
-
Auszug: Ich habe eine Telefonnummer, eine Durchwahlnummer für die Arbeit bei mir zu Hause. Also nicht die Bürodurchwahl sondern noch geheimer, die Nummer landet ja privat hier. Diese Nummer ist nur meiner Firma und einer Hand voll Kunden bekannt. Und diese Nummer nutze ich so selten, dass ich sie nicht mal aus dem Kopf weiß sondern immer in der FritzBox nachgucken muss. Im Telefonbuch oder so steht sie natürlich auch nicht. Eben diese Nummer wird nun schon seit 2 Wochen permanent von Outbound-Callcentern belagert. Wo haben die diese Nummer her? Ich fürchte es hat etwas [damit](http://www.heise.de/security/Alter-Raub-neuer-Skandal-17-Millionen-Telekom-Nummern-entwendet--/news/meldung/116913) oder einem noch unbekannten Fall zu tun... :-( …
Weiter lesen: Durchwahl
- 01.11.08: Autsch
-
Auszug: Es gibt nur eine Sache, die noch schlimmer ist als ein Legostein, auf den man morgens tritt: …
Weiter lesen: Autsch
- 10.10.08: Der Mythos vom statischen Servieren
-
Auszug: In Djangos Dokumentation wird empfohlen, statische Dateien durch den Webserver ausliefern zu lassen. Was bedeutet das? Django läuft doch auf dem Webserver.  Im Prinzip geht es darum, Dateien die Django nicht bearbeiten muss, nicht durch den Django-Prozess zu schleifen. Statische Dateien sollen also den direkten Weg von der Festplatte zum Webserver finden (grüne Linie). Das -- ich nenne es mal -- Problem ist, dass Django eine Funktion bereit stellt, statische Dateien durch Django zu schleifen: [How to serve static files](http://docs.djangoproject.com/en/dev/howto/static-files/).  Hierbei wandern alle statischen Dateien durch den Django-Prozess anstatt dass sie direkt an den Webserver geliefert werden. Das ist ganz klar ineffizient. Warum gibt's das dann? In erster Linie ist es für Entwickler die den [Devserver](http://docs.djangoproject.com/en/dev/ref/django-admin/#runserver-optional-port-number-or-ipaddr-port) nutzen gedacht und **während der Entwicklung** keinen zusätzlichen Webserver zum Static-Serving aufsetzen möchten. Fazit also: Vermeide Static-Serving durch Django um jeden Preis -- es gibt natürlich Ausnahmen, siehe weiter unten. ### PHP macht das doch auch so ### > Letzens hatte mich ein PHP-Entwickerl auch gefragt, wieso er da nicht Django > nehmen kann, dass geht doch auch mit PHP. Oberes ist ein Zitat aus dem [#django-de](http://www.django-de.org/) Channel. PHP macht es aber (unbewusst) genauso wie Django. Statische Dateien werden auch direkt vom Webserver geliefert und nicht erst durch den PHP-Prozessor geschleift. Dieser verarbeitet im Normalfall nur Dateien mit der Endung *.php*. Ein Static-Serving *durch* PHP würde etwa so aussehen, dass der Webserver ein Stylesheet mit 'GET /stylesheet.php' anfordert, und diese Datei das unten gezeigte Script ausführt. #!php Ziemlich dämlich, oder? Aber leider alles schon gesehen. :-( ### Spezialisten ### > Überlasse das Static-Serving einem Server der besser dafür geeignet ist, zum > Beispiel nginx oder Lighty. Den oberen Satz hört man sehr häufig wenn es es Static-Serving geht. Was ist gemeint? Zuerst einmal, [nginx](http://nginx.net/) und [lighttpd](http://www.lighttpd.net/), oder auch liebevoll *lighty* genannt, sind Webserver, die auf das Ausliefern statischer Dateien optimiert sind. Dabei sind sie nicht unbedingt schneller als der Apache, ihre Stärke liegt in der Auslieferung von mehreren tausend Dateien **pro Sekunde**, denn dabei sind sie weniger RAM- und CPU-hungrig und skalieren besser. Es gibt einen Weg, das beste beiden Webservern (Apache für Django, nginx für statische Dateien) herauszuholen. Dabei übernimmt nginx (oder lighty) als **Frontend-Server** alle Anfragen des Webbrowsers und serviert die statischen Dateien direkt. Anfragen auf dynamische Dateien werden an den Apache-Prozess weitergeleitet, der auf einem anderen Port (also nicht :80) lauscht. ([Konfigurationsbeispiel](http://superjared.com/projects/static-generator/#sample_nginx_configuration))  Das Ganze ist dann schon ein recht kompliziertes Setup. Meine persönliche Meinung ist, wenn man den Punkt erreicht hat, dass ein spezialisierter Webserver für statische Dateien nötig ist, sollte man den statischen Kram auf eine eigene Maschine auslagern:  Letztendlich ist alles eine Frage des Budgets. Nicht unerwähnt soll bleiben, dass es auch Firmen gibt, die sich auf das Static-Serving spezialisiert haben. [Akamai](http://www.akamai.de/) ist ein Vertreter aber auch [Amazons S3 Service](http://aws.amazon.com/s3/) ist hervorragend dafür geeignet. ### Was wäre wenn? ### Es gibt natürlich auch das Problem, dass man statische Dateien durch Django schleifen muss. Beispiele: 1. Die Datei soll nur für bestimmte, authentifizierte User verfügbar gemacht werden. Django stellt für solche Fälle eine Möglichkeit zur Verfügung, dass sich der Apache an Djangos Auth-Backend koppeln kann. Mehr Infos dazu in der [Dokumentation](http://docs.djangoproject.com/en/dev/howto/apache-auth/#authenticating-against-django-s-user-database-from-apache). 2. Die Datei wird dynamisch erzeugt, zum Beispiel ein PDF-Export der aktuellen Seite. Vielleicht wäre hier eine Lösung, die Datei im Medienordner zu speichern und einen Location-Header zu dieser Datei zu senden. Dann übernimmt der Webserver wieder das Ausliefern der statischen Dateien und man erhält zudem ein statisches Caching der Dateien. Jedenfalls, wenn es denn nicht anders möglich ist versuche große Dateien als [Iterator](http://docs.djangoproject.com/en/dev/ref/request-response/#passing-iterators) an das Response-Objekt zu übergeben. Damit wird nicht die gesamte Datei in den Prozess geladen (bei einer 80MB Datei wäre der wsgi-Prozess dann 90MB+ groß) sondern in kleinen Häppchen (Chunks) durchgeschleift. Beachte dazu aber, dass die GZip-Middleware diesen Prozess zerstören kann: Ticket [#7581](http://code.djangoproject.com/ticket/7581). Aber auch dafür gibt es eine einfache, performante [Lösung](http://www.django-hosting.de/wiki/ApacheModDeflate/). :-) **Noch ein wichtiger Nachtrag:** Auch wenn in den Bildern mod_python angegeben ist, empfehle ich euch doch ganz stark [mod_wsgi](http://code.google.com/p/modwsgi/) als Kleber zwischen Django und Apache. Es ist resoucen schonender, performanter und allgemein sympathischer. Leider aber hat mod_wsgi kein Logo das ich benutzen konnte. ;-) ### Mehr dazu ### Mehr Lesestoff und vor allem Konfigurations-Beispiele für verschiedene Webserver-Szenarien findest du im deutschsprachigen [django-hosting Wiki](http://www.django-hosting.de/wiki/WikiIndex/). …
Weiter lesen: Der Mythos vom statischen Servieren
- 07.10.08: Kabelsalat
-
Auszug: Aufgrund interner Restrukturierungsmaßnahmen (aka Möbelrücken) war ich gezwungen, mein jahrlang, mühsam aufgebautes Kommunikationsnetzwerk neu zu ordnen: **Vorher:** **Nachher:** Übrig geblieben ist eine alte Fritzbox, die ich aber zur Sicherheit behalten werde, falls die jetzige mal Ihren Geist aufgibt. Daneben ein Fon-Wlan-Point den ich aufgrund der derzeitigen deutschen Rechtslage (Mitstöhrerhaftung, Vorratsdatenspeicherung) schon seit längerer Zeit nicht mehr in Betrieb hatte. Aber da hat sich wohl [was geändert](http://www.heise.de/newsticker/Bundesregierung-Keine-Vorratsdatenspeicherung-fuer-kleine-Hotspot-Betreiber--/meldung/116556). Und dann noch mein *Draytek We2200*, ein klasse Router mit Wlan, VPN und jedem Quatsch den sich ein Netzwerkadmin wünschen kann. Ich hatte gehofft, ihn zu einem fairen Preis noch bei eBay verhökern zu können. Dort wird er aber um die 4 Euro gehandelt, Neupreis damals 300,- DM. :-( Ist ja auch klar, das WLan unterstützt nur WEP-Verschlüsselung und ist damit praktisch unbrauchbar. Leider ist es der beste WLan-Router den ich je hatte, durch seine 2 anständigen, großen Antennen erreicht es beste Ausleuchtung. Dann bleibt er eben auch bei mir, gammelt erstmal im Keller. Wer weiß wofür ich ihn nochmal brauche. :-) Achja, und ein Hoch auf farbige Netzwerkkabel. Die beste Erfindung seit der Spülmaschine. …
Weiter lesen: Kabelsalat
- 29.09.08: Forms: Dynamisch Felder hinzufügen
-
Auszug: 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: #!python 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: #!pycon >>> f = Userform() >>> print f.as_ul() Username: Passwort: >>> f = UserformKontaktdaten() >>> print f.as_ul() Username: Passwort: Vorname: Nachname: ### 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: #!python 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: #!pycon >>> f = Userform() >>> print f.as_ul() Username: Passwort: >>> f = Userform(kontaktdaten=True) >>> print f.as_ul() Username: Passwort: Vorname: Nachname: …
Weiter lesen: Forms: Dynamisch Felder hinzufügen
- 22.09.08: Frettchen an die Macht?
-
Auszug: 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...  …
Weiter lesen: Frettchen an die Macht?
- 22.09.08: Spass mit Newforms-Admin - Read-Only Felder
-
Auszug: In [Automatische Felder](/weblog/spass-mit-newforms-admin-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: #!python 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. #!python 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: #!python 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 %s" % (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 `` 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](http://www.djangosnippets.org/snippets/937/). 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. :-) …
Weiter lesen: Spass mit Newforms-Admin - Read-Only Felder
- 15.09.08: Multiple Aktionen in Djangos Administration - Reloaded
-
Auszug: Vor mehr als einem halben Jahr hatte ich einen Blogpost zum Thema [Multiple Aktionen in Djangos Administration](http://www.mahner.org/weblog/multiple-aktionen-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](http://code.google.com/p/django-batchadmin/) von [Brian Beck](http://blog.brianbeck.com/) 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](http://jquery.com/) benötigt. …
Weiter lesen: Multiple Aktionen in Djangos Administration - Reloaded
- 11.09.08: Zeig mal den Query
-
Auszug: Oft will man schnell mal wissen, was Djangos [DB-Api](http://docs.djangoproject.com/en/dev/ref/models/querysets/) 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](http://github.com/robhudson/django-debug-toolbar/tree/master). Der Nachteil ist, dass die Ausgabe zumeist im Template stattfindet. Im Quellcode direkt gehts aber auch: #!pycon >>> 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! :-) …
Weiter lesen: Zeig mal den Query
- 10.09.08: Merkzettel für Git
-
Auszug: Ein paar Stichpunkte zum Umgang mit [git](http://git.or.cz/); 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-rm` löscht die Dateien wirklich endgültig von der Platte und entfernt sie nicht nur aus der Versionskontrolle. :-( - `git-gui` und `gitk` sehen schrecklich aus, sind aber mächtig und produktiv. - [Giggle](http://developer.imendio.com/projects/giggle) sieht gut aus, kommt aber nicht an `gitk` ran. - `projekt/.gitignore` ist unnötig, 90% der Einträge gehören in die `~/.gitignore` - Subversion nervt immer mehr. `git-svn` erst recht. - [Github](http://github.com/) ruled! - [meld](http://meld.sourceforge.net/) ist das beste diff-Tool des Planeten. Zu guter letzt noch meine .gitconfig: [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: *.pyc *.pyo *~ *.swp *.orig .project .pydevproject .DS_Store MANIFEST dist …
Weiter lesen: Merkzettel für Git
- 25.08.08: Spass mit Newforms-Admin - Automatische Felder
-
Auszug: Im letzten Artikel [Rowlevel-Permissions](/weblog/spass-mit-newforms-admin-rowlevel-permissions/) ging es darum, wie man Einträge nur vom Benutzer selber bearbeiten lassen kann. Wer den Artikel verfolgt und aufgebaut hat, wird festgestellt haben, dass die ganze Sache einen gewaltigen Haken hatte: In der Detailansicht konnte das Autorenfeld frei definiert werden, sprich Benutzer A konnte sich für Benutzer B ausgeben und umgekehrt. In diesem Artikel geht es darum, dem eingeloggten Autor automatisch den Artikel zuzuweisen. ### Holzhammer ### Die einfachste Lösung ist: Zeige das Autorenfeld gar nicht erst an und weise beim Speichern des Artikels dem Autorenfeld den eingeloggten User zu. Das Ganze ist schnell geschehen. Ähnlich wie die "newforms save()" Methode stellt auch ModelAdmin eine Methode zum Überschreiben zur Verfügung: #!python class EntryAdmin(admin.ModelAdmin): # Autorenfeld ausblenden (mit #7937 geht das irgendwann mal einfacher) # #7937: http://code.djangoproject.com/ticket/7973 fields = ( 'title', 'content', ) # Formular valdidieren, Autor setzen und speichern def save_form(self, request, form, change): instance = form.save(commit=False) # Nur bei neuen Artikeln den User setzen if not change: instance.author = request.user return instance Fertig ist die ganze Geschichte. Schnell, sicher und kaum zu überlisten. ### Nicht ganz so krass ### Vielleicht ist das aber auch schon einen Schritt zu weit. In meinem aktuellen Projekt möchte ich es trotzdem dem Benutzer überlassen, den Autor zu setzen, allerdings mit einer Kleinigkeit: Der aktuelle Benutzer soll in der Liste schon vorausgewählt sein. Das Ganze ist ein wenig mehr "tricky". In ModelAdmin gibt es wiederrum eine Methode, die vor der Anzeige der Felder aufgerufen wird und mit der man ihr Verhalten und Aussehen (Widgets) beeinflussen kann: *formfield_for_dbfield*. #!python class EntryAdmin(admin.ModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs) # Autorenfeld hat als Vorauswahl den aktuellen User if db_field.name == "author": field.initial = request.user.pk return field In dieser Methode prüfen wir, ob das aktuelle Feld unser "Autorenfeld" ist, dann wird ihm als Startwert (initial) der aktuelle User zugewiesen. Nächster Haken: Der obige Code funktioniert nicht, in der Methode *formfield_for_dbfield* hat man keinen Zugriff auf das request-Objekt. Ein wenig Patching muss her: #!python class EntryAdmin(admin.ModelAdmin): def add_view(self, request, *args, **kwargs): self._request = request return super(EntryAdmin, self).add_view(request, *args, **kwargs) def change_view(self, request, *args, **kwargs): self._request = request return super(EntryAdmin, self).change_view(request, *args, **kwargs) def formfield_for_dbfield(self, db_field, **kwargs): field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs) # Autorenfeld hat als Vorauswahl den aktuellen User if db_field.name == "author": field.initial = self._request.user.pk return field Glücklicherweise ist ModelAdmin flexibel genug, das Problem zu lösen. Die Methoden *add_view* und *change_view* werden vor der Anzeige des Formulares aufgerufen und beiden steht das Request-Objekt zur Verfügung. Das ist genau der richtige Ort, das Request-Objekt als globale Eigenschaft (self._request) der Klasse zuzuweisen. Somit kann man in der formfield-Methode einfach auf das "globale" Request-Objekt zugreifen und mit den paar Handgriffen ist der Benutzer in der Autorenliste vorausgewählt. Mit ein wenig Javascript kann man auch einfach aus diesen beiden Sachen eins machen: Und nicht vergessen: Auch hier ist es wieder möglich mit Permissions zu arbeiten. So könnte ein Administrator immer den Autor setzen (zweite Variante), wo ein "normaler" User kein Autorenfeld zu Gesicht bekommt und automatisch gesetzt wird (erste Variante). …
Weiter lesen: Spass mit Newforms-Admin - Automatische Felder
- 23.08.08: Spass mit Newforms-Admin - Rowlevel-Permissions
-
Auszug: Newforms-Admin ist schon eine Weile verfügbar und daher wird es Zeit, sich einmal mit den Features auseinander zu setzen. Dieser Artikel ist der Anfang einer Reihe von Tipps und Tricks zu Newforms-Admin, die nacheinander alle aufeinander aufbauen. Ein oft gewünschtes Feature im Admin-Interface ist "Lasse den Benutzer nur seine eigenen Einträge bearbeiten", anders ausgedrückt: Rowlevel-Permissions. Eine der einfachsten Aufgaben für das neue Admin-Interface. Fangen wir wieder einmal mit dem Model an, ein kleines Weblog ist ein gutes Beispiel: #!python from django.db import models from django.contrib.auth.models import User class Entry(models.Model): title = models.CharField(max_length=255) content = models.TextField() author = models.ForeignKey(User) def __unicode__(self): return self.title Um das Model mit dem Admin Interface administrieren zu können, ist eine einfache ModelAdmin-Klasse notwendig, bestens aufgehoben in der Datei *admin.py* ([Jahaa](/weblog/newforms-adminoptions-besser-strukturieren/#comment15198)): #!python from django.contrib import admin from myproject.weblog.models import Entry class EntryAdmin(admin.ModelAdmin): list_display = ( 'title', 'author', ) admin.site.register(Entry, EntryAdmin) Soweit nichts spektakuläres, mit ein paar Einträgen von verschiedenen Usern sieht die Übersichtsseite so aus: Nun zur Aufgabe: Um dem Benutzer nur seine Beiträge bearbeiten zu lassen, lässt Newforms-Admin auf einfache Weise zu, das Queryset für die Listenansicht zu bearbeiten. Füge in deine ModelAdmin-Klasse eine Funktion "queryset" hinzu, die die Einträge anhand der User/Autor-Beziehung filtert: #!python class EntryAdmin(admin.ModelAdmin): # ... def queryset(self, request): return self.model._default_manager.filter(author=request.user) Das war es auch schon! Jeder Autor sieht ab sofort nur noch seine eigenen Artikel: Die Sache hat nur einen Haken: Auch Administratoren würden nur ihre eigenen Artikel sehen, irgendwie nicht sehr sinnvoll. Um das Problem zu lösen, schaffen wir uns am einfachsten eine [zusätzliche Permission](http://www.djangoproject.com/documentation/model-api/#permissions). Erweitere also dein Model wie folgt: #!python class Entry(models.Model): # ... class Meta: permissions = ( ('can_view_all', 'Can view all Entries'), ) Nach einem "./manage.py syncdb" ist diese neue Permission auch schon verfügbar, die du deinem User zuweisen kannst: Leider ist nicht genug Voodoo in Django vorhanden, als dass alles automatisch geht. In deiner Queryset-Redefinition musst du noch auf die neue Permission prüfen, etwa so: #!python def queryset(self, request): # ... if request.user.has_perm('weblog.can_view_all'): return self.model._default_manager.get_query_set() return self.model._default_manager.filter(author=request.user) Und fertig. "Normale" User können nun nur noch ihre eigenen Beiträge bearbeiten während Administratoren oder User mit der Permission "can_view_all" alle Einträge angezeigt bekommen. Zu guter letzt noch ein Hinweis: Theoretisch könnte man dieses Verhalten auch umdrehen und eine Permission "Can view only his own entries" (\*urgs\*) schaffen. Das nur fürs Gedächtnis. …
Weiter lesen: Spass mit Newforms-Admin - Rowlevel-Permissions
- 17.08.08: Wiederverwendbare Django-Projekte
-
Auszug: Eine der Stärken von Django ist die Wiederverwendbarkeit von einzelnen Applikationen (aka *reusable apps*). Will man aber ein komplettes Projekt veröffentlichen, sollte man sich auch Gedanken machen, wie man das komplette Projekt möglichst automatisch veröffentlichen kann. Anders ausgedrückt: der User soll möglichst wenig Pfade und Config-Einstellungen von Hand anpassen müssen. Die verschiedensten Einstellungen eines Django-Projekts befinden sich in der Regel in der *settings.py* im Hauptverzeichnis deines Projekts, hier sind ein paar kleine Kniffe, wie du diese Einstellung möglichst dynamisch hälst: **Ein Hinweis zuvor:** Die *settings.py* ist eine Python-Datei und wird auch von Python geparst. Es ist keine Ansammlung statischer Definitionen sondern in ihr kann auch eine komplette Programmlogik ausgelagert werden. ### 1. Dynamische Pfade Die erste Notwendigkeit vor dem Deployment einer Seite ist es, die Pfade zu den Templates und den statischen Mediendateien anzupassen. Diese Arbeit können wir dem User abnehmen, indem wir sie von vornherein dynamisch festlegen. Grundlage allen ist es, erst einmal zu wissen, welcher Pfad zum Projektverzeichnis führt: #!python import os PROJECT_ROOT = os.path.dirname(__file__) Die Variable PROJECT_ROOT enthält damit den Pfad zum aktuellen Verzeichnis, sei es /var/www/meinprojekt/ oder unter Windows C:\\Dokumente und Einstellungen\\username\\meinprojekt\\. Damit ist es möglich, die Pfade zu den Templates und den Mediendateien festzulegen. Eine Arbeit der der User vor dem Deployment also nicht mehr erledigen braucht: #!python MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'site_media') TEMPLATE_DIRS = ( os.path.join(PROJECT_ROOT, 'templates'), ) ### 2. Lass den SECRET_KEY geheim Oft wird es vom User vergessen, den *SECRET_KEY* neu zu setzen. Er wird bspw. als Salt für die Passwort-Generierung benutzt. Am sichersten und einfachsten ist es für den User, den *SECRET_KEY* bei Bedarf sich selbst erstellen zu lassen: (Übernommen vom [Byteflow-Projekt](http://hg.piranha.org.ua/byteflow/file/8b5437fb248a/settings.py#l67)) #!python try: SECRET_KEY except NameError: SECRET_FILE = os.path.join(PROJECT_ROOT, 'secret.txt') try: SECRET_KEY = open(SECRET_FILE).read().strip() except IOError: try: from random import choice SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) secret = file(SECRET_FILE, 'w') secret.write(SECRET_KEY) secret.close() except IOError: Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE) Der kleine Schnipsel prüft, ob eine Variable *SECRET_KEY* schon definiert wurde. Wenn nicht, erstellt er eine Datei *secret.txt* im Projektverzeichnis und schreibt darin selbstständig einen zufälligen String. (Der Webserver muss in diesem Fall Schreibrechte für das Projektverzeichniss besitzen.) ### 3. Don't touch the settings In der Bildbearbeitung gibt es einen Leitsatz: Don't touch the Pixels. Das bedeutet, wenn man ein Bild bearbeiten will, legt man sich zuvor eine Kopie der Bildebene an oder erstellt einen Snapshot, um im Bedarfsfall auf die Originaldaten zurück greifen zu können. Mit den Django-Settings verhält es sich ähnlich. Ich füge in alle meine Settings-Dateien am Ende diesen Schnipsel ein: #!python try: from local_settings import * except ImportError: pass Er versucht, eine Datei "local_settings.py" zu finden und übernimmt/überschreibt die Einstellungen aus dieser Datei in die globalen Einstellungen. Diese "local_settings.py" ist der perfekte Ort für die Einstellungen, die vom User in jedem Fall gesetzt werden müssen -- die Einstellungen für den Datenbankzugriff etwa. Der große Vorteil kommt aber erst zum Tragen, wenn man das Projekt regelmäßig aus einem VCS aktualisiert. Da im Idealfall keine Daten in der ursprünglichen "settings.py" geändert werden, gibt es keine Kollisionen mit Updates. Der User kann also das Projekt regelmässig aus dem VCS aktualisieren, ohne jedes Mal die Einstellungen neu schreiben zu müssen. Das waren drei kleine Kniffe, die das Deployment eines Projekts für den User sehr erleichtern. Das Ganze lässt sich noch beliebig fortführen, ist aber in den meisten Fällen projekt-abhängig. Hab ich noch etwas wichtiges vergessen, dann schreibt doch einen Hinweis in die Kommentare. :-) …
Weiter lesen: Wiederverwendbare Django-Projekte
- 12.08.08: Firefox cached keine Dateien via SSL
-
Auszug: Das Trac-System auf [django-de.org](https://www.django-de.org/trac/wiki) erschien mir immer sehr langsam. 5 Sekunden für eine Seite war die Regel. Trotz dem die Response-Headers brav einen ETag aufwiesen macht Firefox 3 einfach kein Conditional Get, sprich jede Datei (Bilder, Stylesheets etc.), egal wie alt sie sind, werden bei jedem Request neu geladen. Der Grund war dann doch schnell gefunden: Das soll so! Firefox speichert keine Dateien die über http**s** kommen auf der Platte. In gewisser Hinsicht ein Sicherheitsgewinn, für mich in erster Linie aber nervig, da ich Ticketsysteme recht häufig nutze, und diese zumeist über SSL/TLS gehandelt werden. Die Lösung des Problems ist auch schnell gefunden: In den globalen Einstellungen *about:config* setzt man einfach *browser.cache.disk_cache_ssl* von *false* auf *true* und schon cached Firefox auch Dateien von SSL-Quellen auf der Platte. Ob das ein Sicherheitsrisiko darstellt, muss jeder für sich selbst entscheiden. Bugreport und Diskussion auf [bugzilla.mozilla.com](https://bugzilla.mozilla.org/show_bug.cgi?id=309368). …
Weiter lesen: Firefox cached keine Dateien via SSL
- 05.08.08: Generische Dateinamen für generische Apps
-
Auszug: In Django gibt es nur wenige Konventionen, wie man seine Dateinamen (bzw. Python-Module) benennen soll. Wächst das Django-Projekt wird man nicht drumrum kommen, Code in eigene Module auszulagern. So hat es sich z.B. eingebürgert, forms-Formulare in eine Datei "forms.py" der jeweiligen Applikation legen. In [#django-de](irc://freenode.net/django-de) gab es in letzter Zeit häufig Fragen und Diskussionen zu diesem Thema und nun hat [Arne Brodowski](http://www.arnebrodowski.de/blog/) sich die Mühe gemacht, diese Dateinamen einmal aufzulisten und zu beschreiben: [http://www.django-hosting.de/wiki/BestPracticesDateinamen/](http://www.django-hosting.de/wiki/BestPracticesDateinamen/) Um es noch einmal klar zu stellen: Niemand wird gezwungen, diese Dateikonventionen zu verfolgen. Wer aber sein Projekt oder Applikation veröffentlichen will, sie möglichst wiederverwendbar gestalten will, tut gut daran, sich an diese Quasi-Standards zu halten. Diese kommen auch nicht von *irgendwo* sondern sind aus Erfahrung vieler Open-Source Projekte entstanden. Danke Arne für die Liste, vielleicht währe es eine Gute Idee, wenn diese Informationen auch im offiziellen [Django-Wiki](http://code.djangoproject.com/wiki) Einzug halten würden. …
Weiter lesen: Generische Dateinamen für generische Apps
- 24.07.08: Zeile verdoppeln in Eclipse
-
Auszug: Im Editor [Scite](http://www.scintilla.org/SciTE.html) gibt es ein wunderbar kleines Feature: Mit der Tastenkombi CTRL+D verdoppelt man die aktuelle Zeile oder Auswahl. Diese Funktion braucht man vielleicht nicht so oft in der Programmierung, ich vermisse sie aber insbesondere im Umgang mit Javascript und HTML. Leider ist in den meisten anderen IDEs diese Tastenkombination mit "Zeile löschen" voreingestellt was einfach nur nervt. Wolfram Kriesing hat nun [eine Möglichkeit](http://blog.uxebu.com/2008/06/26/duplicate-lineselection-macro-for-komodo/) gezeigt, wie man [Komodo](http://www.activestate.com/Products/komodo_ide/index.mhtml) dieses Verhalten beibringt und gleichzeitig habe ich noch einmal geschaut, ob Eclipse/Aptana das nicht auch irgendwie unterstützt. Und siehe da, es geht sogar von Haus aus. CTRL+ALT+UP ist die Standardkombi dafür, die sich natürlich einfach neu mappen lässt. :-) …
Weiter lesen: Zeile verdoppeln in Eclipse
- 23.07.08: Newforms AdminOptions besser strukturieren
-
Auszug: Wer in den letzten Tagen seine schon etwas ältere Django-trunk Version aktualisiert hat, dürfte sein blaues Wunder erlebt haben, positiv oder negativ. Unter anderem hat [Newforms Admin](http://www.djangoproject.com/documentation/admin/) ja endlich Einzug gehalten. Anders als bisher werden Einstellungen der Models nun nicht mehr in einer Subklasse *Admin* des Models definiert, sondern in einer eigenen: #!python class Entry(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(blank=True, db_index=True) content = models.TextField() published = models.DateTimeField(auto_now_add=True) class EntryAdmin(admin.ModelAdmin): save_on_top = True list_select_related = True date_hierarchy = 'published' list_display_links = ('title') search_fields = ('title', 'content') admin.site.register(Entry, EntryAdmin) Nun gibt es prinzipiell zwei Möglichkeiten, diese Admin-Optionen an die Models zu "binden": - [autodiscover()](http://www.djangoproject.com/documentation/admin/#hooking-adminsite-instances-into-your-urlconf) durchsucht die Apps nach einer Datei "admin.py" und importiert dieses Modul automatisch. - [Per Hand](http://www.djangoproject.com/documentation/admin/#modeladmin-objects) und überall mittels "admin.site.register('model', 'options')" wie im obigen Beispiel. Ich bevorzuge den zweiten Weg, da ist weniger Voodoo im Spiel und ich kann penibel festlegen, welches App im Admin erscheint und welche Optionen ihm zugewiesen werden. Bei meinem aktuellen Projekt habe ich dafür eine Basisklasse "DefaultModelAdmin" für alle ModelAdmins erstellt und leite davon alle anderen ModelAdmin-Klassen ab: #!python # File: apps/adminprefs/defaults from django.contrib import admin class DefaultModelAdmin(admin.ModelAdmin): save_on_top = True list_select_related = True # File: models.py/admin.py from django.contrib import admin from adminprefs.defaults import DefaultModelAdmin class EntryAdmin(DefaultModelAdmin): date_hierarchy = 'published' list_display_links = ('title') search_fields = ('title', 'content') admin.site.register(Entry, EntryAdmin) Das ist nicht nur DRY sondern wird in Zukunft auch helfen, wenn einmal ein zentraler Eingriff für alle Admin-Sachen nötig ist. …
Weiter lesen: Newforms AdminOptions besser strukturieren
- 22.07.08: ModelsForms mit Choices validiert nicht korrekt
-
Auszug: Um es vorweg zu nehmen, dies ist die Beschreibung eines Bugs in Django. Ja, dafür gibt es schon [ein Ticket](http://code.djangoproject.com/ticket/6967) und nein, es ist derzeit noch nicht im Trunk gefixt. Oki, wer bis hierhin durchgehalten hat, weiter gehts: :-) Angenommen wir haben dieses simple Model, die Werte des Testfeldes bestehen aus den vorher definierten Choices. #!python from django.db import models CHOICES = ( (u'Bube', u'Bube'), (u'Dame', u'Dame'), (u'Koenig', u'Koenig'), (u'Ass', u'Ass'), ) # Das Model class TestModel(models.Model): testfeld = models.CharField(max_length=30, choices=CHOICES) Um ein simples Formular daraus zu erstellen ist ModelForms ideal: #!python from testprojekt.models import TestModel from django import forms # Django 1.0 Alpha wohooo! class TestForm(forms.ModelForm): class Meta: model = TestModel Einfacher gehts kaum, mit den paar Zeilen hat man schon ein komplettes Formular parat. Aber dass wußtest du sicher schon, also weiter. Basteln wir uns eine einfache Ausgabe: #!pycon >>> f = TestForm() >>> f.as_ul() # Beispielausgabe Bube Dame Koenig Ass Alles korrekt bis hier hin. Angenommen das Formular wird nun auf der Webseite angezeigt, der User wählt "Bube" und schickt das Formular ab: #!pycon >>> f = TestForm(data={'testfeld': 'Bube'}) >>> f.as_ul() Bube Dame Koenig Ass >>> f.is_valid() True Das Formular ist valide, weil "Bube" ja eine korrekte Auswahl für das Feld 'testfeld' ist. Aber zur Erinnerung: SELECT-Felder in HTML sind auch nur stinknormale Datenfelder und der Browser prüft **nicht**, ob der abgesendete Wert auch des Feldes auch vorher in den SELECT-Options definiert wurde. Ändern wir also mal die Werte der Formulardaten: #!pycon >>> f = TestForm(data={'testfeld': 'Boeses Maedchen'}) >>> f.as_ul() Bube Dame Koenig Ass >>> f.is_valid() True Wem das nicht schlimm erscheint: Diese Daten speichert das Formular leider auch in die Datenbank, das Model selbst validiert also nicht noch einmal nach. #!pycon >>> objekt = f.save() >>> objekt.testfeld u'Boeses Maedchen' Ganz fatal in meinen Augen. Zur Erinnerung, eigentlich sollte das *testfeld* im Model nur die Daten Bube, Dame, Koenig und Ass annehmen. Wie kann man eigentlich "fremde" Daten in ein Select-Feld schreiben? Nichts einfacher als das, die bekannte Firefox-Extension "Web Developer Toolbar" hat eine eigene Funktion dafür: Mein derzeitiger Lösungsvorschlag sieht so aus, dass ich einfach mit einer clean-Methode auf das Vorhandensein der Eingabe in den Choices prüfe: #!python from testprojekt.models import TestModel, CHOICES from django import forms class TestForm(forms.ModelForm): def clean_testfeld(self): if not self.cleaned_data['testfeld'] in dict(CHOICES): raise forms.ValidationError(u'%s is not a valid choice' % (self.cleaned_data['testfeld'])) return self.cleaned_data['testfeld'] class Meta: model = TestModel Dann klappts auch wie gewünscht. #!pycon >>> f = TestForm(data={'testfeld': 'Boeses Maedchen'}) >>> print f.is_valid() False >>> f.as_ul() errorlist: "Boeses Maedchen is not a valid choice" Bube Dame Koenig Ass
Weiter lesen: ModelsForms mit Choices validiert nicht korrekt
- 21.07.08: Resignation
-
Auszug: > Ich will das aber so. Ist der Satz von einem Kunden, ab dem man einfach resignieren muss und auch die sinnfreiesten Änderungen durchführen sollte. Mehr schreibe ich dazu nicht -- zu deprimierend -- nur so viel: Ein dritter "Usability-Profi" hatte seine Finger im Spiel. > Das machen aber alle so. …
Weiter lesen: Resignation
- 03.07.08: Gib mir Pagerank
-
Auszug: Es ist weder Montag noch Freitag. Warum werde ich dann heute mit solchen Kundenanfragen gestraft? > warum haben wir auf unserer hp kein pakerank ?? > wenn moeglich bitte schnellstens einrichten ! Das ist [Pagerank](http://de.wikipedia.org/wiki/PageRank). …
Weiter lesen: Gib mir Pagerank
- 26.06.08: Wie bei newforms die Validierung abläuft
-
Auszug: [Djangos newforms](http://www.djangoproject.com/documentation/newforms/) Formularen stehen 3 Methoden zur Verfügung, Eingabedaten zu prüfen. - **Feld.clean():** Die clean-Methode des Feldes selbst. Django bringt für die jeweiligen Felder schon grundsätzliche Prüfungen mit. In ein IntegerField dürfen eben nur Ziffern eingegeben werden und ein EmailField prüft die korrekte Syntax der E-Mail-Adresse. In der Regel ist es nicht notwendig, bestehende Feld-Clean-Methoden zu ändern, denn dafür gibt es: - Formular.clean_\(): In dieser Funktion kannst du penibel festlegen, welche Daten für das Feld valide sind und einen Fehler ausgeben oder diese Daten ändern. - **Formular.clean():** Diese Funktion wird am Ende ausgeführt, wenn alle Felder einzeln schon geprüft worden sind. In allen Funktionen hat das Formular stets Zugriff auf die eingegeben Daten, denn diese stehen im Dict *self.cleaned_data*. Wofür ist also die "globale" clean() Methode gut? Dazu ist es wichtig zu wissen, wie der Ablauf der Validierung von Newforms-Formularen stattfindet. Vergleiche einmal dieses Formular und dessen Ausgabe: #!/usr/bin/env python # -*- coding: utf-8 -*- from django import newforms as forms class Form(forms.Form): name = forms.CharField() plz = forms.IntegerField() ort = forms.CharField() def clean_name(self): print "Vorhandene Daten bei 'name': %s" % self.cleaned_data return self.cleaned_data['name'] def clean_plz(self): print "Vorhandene Daten bei 'plz': %s" % self.cleaned_data return self.cleaned_data['plz'] def clean_ort(self): print "Vorhandene Daten bei 'ort': %s" % self.cleaned_data return self.cleaned_data['ort'] def clean(self): print "Die Formular clean() Methode. Spätestens jetzt sind alle Daten" \ "vorhanden: %s" % self.cleaned_data return self.cleaned_data test_data = {'name': 'Martin', 'plz': '18581', 'ort': 'Putbus'} f = Form(test_data) f.as_ul() Ausgabe: >python -u test.py Vorhandene Daten bei 'name': {'name': u'Martin'} Vorhandene Daten bei 'plz': {'name': u'Martin', 'plz': 18581} Vorhandene Daten bei 'ort': {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581} Die Formular clean() Methode. Spätestens jetzt sind alle Datenvorhanden: {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581} Zur Erläuterung. Die Validierung im Formular geht der Reihe nach, anhand der Felder von oben nach unten. In diesem Formular ist es also dieses Schema: 1. 'name' den Wert aus den Daten zuweisen 2. name.clean() - Die interne clean-Funktion eines CharFields 3. f.clean_name() - Unsere selbst definierte clean-Funktion für 'name' 4. 'plz' den Wert aus den Daten zuweisen 5. plz.clean() - Die interne clean-Funktion eines IntegerFields 6. f.clean_plz() - Unsere selbst definierte clean-Funktion für 'plz' 7. 'ort' den Wert aus den Daten zuweisen 8. ort.clean() - Die interne clean-Funktion eines CharFields 9. f.clean_ort() - Unsere selbst definierte clean-Funktion für 'ort' 10. f.clean() - Die "globale" Clean-Methode wird erst am Ende ausgeführt. In der Ausgabe siehst du auch, das dir in der Funktion "clean_name" (unser erstes Feld) noch gar keine Werte für die Felder "plz" und "ort" zur Verfügung stehen. Nochmal: die Validierung *und* Zuweisung der Daten läuft von oben nach unten, in der Reihenfolge der Felder. Warum kann man also schon im ersten Feld nicht auf alle Eingabedaten zugreifen? Hypothese: Wozu soll das Formular auch alle Datenfelder durchlaufen, wenn schon im ersten Feld 'name' ein Fehler (ValidationError) auftreten kann. ### Aha, toll ### Ok, kommen wir endlich zum springenden Punkt für diesen Artikel. In der Regel wird ein Formular nicht nur *pro Feld* validiert sondern auch in Kombination. Beispiele währen: - 2 Passwort-Felder müssen das selbe Passwort haben - Möchte ich per Lastschrift zahlen, muss ich auch Kontodaten hinterlegen - Wünsche ich Antwort per E-Mail, muss ich auch eine E-Mail-Adresse hinterlegen, sonst reicht mein Name Und so weiter, das ganze hängt ja vom Anwendungsfall ab und die Möglichkeiten so eines Formulares sind unbegrenzt. Nun *endlich* der springende Punkt, und ich würde das ganze hier nicht schreiben, wenn es nicht ein häufiges (Anfänger)Problem währe: Willst du **mehrere Felder** miteinander vergleichen gehört diese Logik in die **clean()**-Funktion des Formulares, nicht in die des Feldes! Der Grund dafür steht ja schon oben. Nun aber das **Aber**. Der Grund warum so viele Leute die Validierungslogik in eine clean_\ Funktion packen -- obwohl es mehrere Felder betrifft -- ist, dass Fehlermeldungen aus der clean() Funktion immer ganz oben, vor allen Feldern angezeigt werden. Die so genannten *non_field_errors*, also Fehlermeldungen die nicht an ein Feld gebunden sind. Hier ein Beispiel dass die Felder 'ort' und 'name' miteinander prüft: #!/usr/bin/env python # -*- coding: utf-8 -*- from django import newforms as forms class Form(forms.Form): name = forms.CharField() plz = forms.IntegerField() ort = forms.CharField() def clean(self): if self.cleaned_data['name'] == 'Meier' and \ self.cleaned_data['ort'] == 'Berlin': raise forms.ValidationError('Es gibt keinen "Meier" in Berlin.' \ 'Bitte legen Sie sich einen anderen Namen zu.') test_data = {'name': 'Meier', 'plz': u'10627', 'ort': 'Berlin'} f = Form(test_data) print f.as_ul() In der Ausgabe des Formulares steht der Fehler ganz oben: Es gibt keinen "Meier" in Berlin.Bitte legen Sie sich einen anderen Namen zu. Name: Plz: Ort: Möchte man aber im Nachhinein noch einem Feld einen Fehlertext zuweisen, so geht das einfach über das Dict self.errors[\] (es erwartet für das Feld ein Liste oder Tupel, über die es iterieren kann): #!/usr/bin/env python class Form(forms.Form): # ... def clean(self): if self.cleaned_data['name'] == 'Meier' and \ self.cleaned_data['ort'] == 'Berlin': self.errors['name'] = ('Es gibt keinen "Meier" in Berlin.',) raise forms.ValidationError('Es sind Fehler aufgetreten:') In der Ausgabe hat unser Feld nun seine eigene Fehlermeldung: Es sind Fehler aufgetreten: Es gibt keinen "Meier" in Berlin. Name: Plz: Ort: So weit... viel Spass weiterhin mit newforms. :-) …
Weiter lesen: Wie bei newforms die Validierung abläuft
- 19.06.08: Python difflib Praxisbeispiel
-
Auszug: Für eine Communityseite möchte ich, wenn ein Beitrag geändert wird, dem Vorautor eine E-Mail mit den Änderungen schicken. Die klassische Ausgabe eines [Diffs](http://de.wikipedia.org/wiki/Diff) gefällt mir besonders gut. Hier ist ein kleines Beispiel wie das Pythonmodul *difflib* zu gebrauchen ist: #!/usr/bin/env python # -*- coding: utf-8 -*- import difflib a = """ Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus tincidunt augue, quis ullamcorper lorem pede a ante. """ b = """ Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus tincidunt augue, quis ullamcorper lorem pede a onte! """ d = difflib.unified_diff( a.splitlines(), b.splitlines(), 'Original', 'Current', lineterm='' ) print '\n'.join(d) Und die Ausgabe: --- Original +++ Current @@ -1,4 +1,4 @@ -Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed +Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus -tincidunt augue, quis ullamcorper lorem pede a ante. +tincidunt augue, quis ullamcorper lorem pede a onte! Wer eher nach einer farbigen, detailierten Ausgabe wie bei Trac sucht (hier im Beispiel übernimmt [Pygments](http://pygments.org/) im Nachhinein das Highlighting), sollte sich auch das [HtmlDiff Modul](http://docs.python.org/lib/module-difflib.html#l2h-457) anschauen. …
Weiter lesen: Python difflib Praxisbeispiel
- 15.06.08: Zusätzliche Templatedaten in newforms-Formulare einfügen.
-
Auszug: Ok, das ist schon ein kryptischer Titel aber es ist etwas ganz Einfaches gemeint. Djangos [newforms](http://www.djangoproject.com/documentation/newforms/) Formulare lassen sich auf einfache und schnelle Weise mit den Methoden *as_ul()*, *as_p()* und *as_table()* ausgeben. Leider werden die Felder dann aber der Reihe nach ausgegeben, einen Zwischentitel oder ähnliches einzufügen ist dann nicht mehr möglich. {{ form.as_ul }} Angenommen im Formular befinden sich folgende Felder: - username - emailadresse - passwort - passwort (wiederholung) - Vorname - Nachname - Homepage-URL Wobei die ersten 4 Felder Pflichtfelder sind, die letzten 3 aber optional. Über diese letzten 3 Felder möchte ich eine Titelzeile *"Die folgenden Felder sind optional"* setzen. Nun ist es einfacher weise möglich, das Formular *per Hand* aufzubauen: {{ form.username.label_tag }} {{ form.username }} {{ form.username.help_text}} ... Die folgenden Felder sind optional {{ form.first_name.label_tag }} {{ form.first_name }} {{ form.first_name.help_text}} ... Sicherlich erreicht man so die maximale Flexibilität aber es ist auch fehleranfällig wenn sich ein Feld(name) ändert und es muss immer Hand angelegt werden, wenn ein neues Feld hinzukommt oder entfernt wird. Also zurück zu den direkten Formular Ausgabemethoden: {{ form.as_ul }} Über die Felder im Formular lässt sich auch iterieren! *form.as_ul()* ist in der Ausgabe identisch zu diesem Schnipsel: {{ form.non_field_errors }} {% for field in form %} {% if field.errors %}{{ field.errors }}{% endif %} {{ field.label_tag }} {{ field }} {% if field.help_text %}{{ field.help_text }}{% endif %} {% endfor %} Ich wollte ja über die letzten 3 Felder eine Titelzeile *"Die folgenden Felder sind optional"* setzen. Das erste der 3 Felder ist "Vorname" und da wir wissen wie dieses Feld heißt, können wir auch darauf prüfen. {{ form.non_field_errors }} {% for field in form %} {% ifequal field.label "First name" %} Die folgenden Felder sind optional. {% endifequal %} {% if field.errors %}{{ field.errors }}{% endif %} {{ field.label_tag }} {{ field }} {% if field.help_text %}{{ field.help_text }}{% endif %} {% endfor %} Auf diese Weise lassen sich einfach Daten vor, nach oder selbst in bestimmte Felder einfügen -- ohne dass die Flexibilität von newforms Formularen gefährdet ist. …
Weiter lesen: Zusätzliche Templatedaten in newforms-Formulare einfügen.
- 29.05.08: Samstag == Fernsehtag
-
Auszug: Denn am Samstag kommt Fußball und -- das mittlerweile bei uns fast religiös verehrte -- Schlag den Raab. (Übrigens IMHO die beste Fernsehshow derzeit im deutschen Fernsehen.)  Das bedeutet 7 Stunden geballtes Fernsehen, mehr als ich wahrscheinlich in der ganzen Woche gucke. Ich denke ich werde mir mal einige Joga-Übungen aneignen, wie soll meine Pomuskulatur das sonst nur aushalten... Ein super Übersicht über das Fernsehprogramm und diese Merkliste gibts bei [tvtv.de](http://www.tvtv.de). …
Weiter lesen: Samstag == Fernsehtag
- 29.05.08: Kontaktformulare leicht gemacht
-
Auszug: Das Feedback der Danke-Seite eines Kontaktformulares (Also nochmal zur Sicherheit, das ist die Seite, die *nach* dem Abschicken des Formulares angezeigt wird.): > Sollten Sie das Emailfeld nicht korrekt ausgefüllt haben, wird Ihre Anfrage nicht an uns weitergeleitet. > Das Anfrageformular überprüft die Emailadresse und den angegebenen Hostnamen auf seine Richtigkeit. Sollte dies nicht der Fall sein wird Ihre Anfrage nicht an uns weitergeleitet! So ist es ja auch nicht schlecht. Spart viel Arbeit und die elende Eingabeüberprüfung macht sowieso keinen Spaß. Wozu auch Feedback an den User, der weiß doch ganz genau, dass seine E-Mail korrekt eingetragen wurde und dass sein Hostname (WTF?) seine "Richtigkeit" hat. :-D \*augenverdreh\* …
Weiter lesen: Kontaktformulare leicht gemacht
- 26.05.08: Endlich mehr Spam
-
Auszug: Am Samstag Abend hatte ich eine Domain von 1&1 übernommen plus ein dazugehöriges Mailkonto. Während mein Postfix Mailserver bisher eher vor sich hindümpelte bringt dieses Konto nun endlich einmal Leben in die Bude. :-)  Bei dieser Domain ist E-Mail nun endlich wieder nutzbar. …
Weiter lesen: Endlich mehr Spam
- 15.05.08: Störende Schatten unter Compiz entfernen
-
Auszug: Hier unter Ubuntu/Gnome setze ich Compiz als Desktopmanager ein, welche mir zahlreiche Effekte und unter anderem auch Schatten unter den Fenstern bietet. Einige Dinge stören mich aber an diesen Schatten: Wenn man ein Fenster maximiert, liegt der Schatten des oberen Panels über dem Fenster.  Und der Schatten unter den Tooltip (die gelben Blasen die auftauchen, wenn man den Cursor etwas länger über ein Objekt hält) sind auch viel zu groß.  Der Schatten lässt sich aber penibel für jedes Objekt (Dockleisten, Fenster, Tooltips etc.) an- oder ausschalten. Installiere dir dazu den *Compiz Settingsmanager*: sudo apt-get install compizconfig-settings-manager Im Menü unter System > Einstellungen > "Erweiterte Einstellungen für Desktop Effekte" findest du diesen Manager in dem du fünf Trillionen Einstellungsmöglichkeiten für Compiz findest, unter anderem auch die, für die Schatten. Für die Schatten ist das Modul *Window Decoration* zuständig, im letzten Feld *Shadow Windows* kannst du einstellen, welche Objekte ein Schatten erhält. Um das Dock und die Tooltips von Schatten zu befreien, ändere den Wert "any" in: any & !(type=dock | type=tooltip)  So haben bei mir weiterhin die Fenster und Menüs einen Schatten, das Panel und die Tooltips aber nicht mehr. Lieber währe mir natürlich, das man den Schatten für die Tooltips eigentständig setzen könnte (ein ganz leichter) und der Schatten unter dem oberen Dock unter das Fenster fällt (wie bei der Menüleiste eines Macs). …
Weiter lesen: Störende Schatten unter Compiz entfernen
- 27.04.08: APPEND_SLASH und PREPEND_WWW im Webserver
-
Auszug: Djangos per Default aktive Middleware [CommonMiddleware](http://www.djangoproject.com/documentation/middleware/#django-middleware-common-commonmiddleware) stellt zwei Parameter zur Verfügung, mit denen sich das Verhalten aller URLs ändern lässt. - **[PREPEND_WWW](http://www.djangoproject.com/documentation/settings/#prepend-www)** leitet eine Anfrage auf eine Seite ohne voran gestelltes www "http://example.org/" automatisch zur Domain mit www "http://www.example.org/" um. - **[APPEND_SLASH](http://www.djangoproject.com/documentation/settings/#append-slash)** fügt der URL, wenn sie nicht mit einem Slash endet, diesen an. Das ist auch das normale Verhalten der meisten Webserver, wenn ein Verzeichnis angefordert wird. Beides ist eine Aufgabe die der Webserver schneller und wahrscheinlich resourcenschonender erledigen kann. Wer [mod_rewrite](http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html) seinem Apache zur Verfügung stellt, kann diese beiden Optionen auch direkt erledigen lassen. Ohne den mod_wsgi/mod_python Unterbau und ohne dass eine Codezeile von Django ausgeführt wird. Dieser Schnipsel gehört in die VHost deiner Domain -- oder je nach Aufbau deines Django-Projekts auch in eine .htaccess Datei des entsprechenden Ordners: # Voodoo-Engine starten RewriteEngine On #RewriteLog /path/to/rewrite.log #RewriteLogLevel 9 # Prepend WWW RewriteCond %{HTTP_HOST} !^www\. RewriteRule ^/(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L] # Append Slash RewriteCond %{REQUEST_URI} !\. RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_URI} !.*/$ RewriteRule ^(.*)$ $1/ [R=301,L] Nützlich ist dieser Schnipsel auch allen, die Djangos [Flatpages](http://www.djangoproject.com/documentation/flatpages/) einsetzen. Wenn der User eine URL besucht, die auf eine Flatpage zeigt, aber den Slash am Ende vergisst, bekommt er nur einen 404-NotFound Fehler zurück. Ein Bug der schon etwas länger diskutiert wird und hoffentlich in Zukunft auch gefixt wird. Einen Patch [gibt es](http://code.djangoproject.com/ticket/6213) jedenfalls schon. …
Weiter lesen: APPEND_SLASH und PREPEND_WWW im Webserver
- 04.04.08: Das Internet vergisst nie
-
Auszug: Und Google weiß alles. Ich hab mal in den alten GoogleGroups Archiven geschnüffelt und Beiträge von mir aus dem Jahre 1997 heraus gefischt. Krass, und das sind nicht einmal die ältesten sondern nur die paar Beiträge aus dem [Z-Netz](http://de.wikipedia.org/wiki/Z-Netz). Damals war ich noch mit einem 19.200 Baud-Modem (später dann 33k, yeah!) unterwegs. Das Internet (WWW mit Browser und so) gabs noch nicht. Naja für mich nicht, ich glaube der Stundenpreis der Telekom lag um die 4 D-Mark. Trotzdem hatte ich schon Kontakt zu Leuten mit einer ominösen *@gmx.de Adresse. Ich wußte jahrelang nicht, was das sein sollte. Newsgroups und elektronische Erzeugnisse gabs für mich von einer Mailbox die an das Netz der [Uni Greifswald](http://www.uni-greifswald.de/) angeschlossen war. Die lag im noch *relativ* günstigen Regio200 Tarif der Telekom -- trotzdem -- wenn ich mir ein MP3 laden wollte, konnte ich die CD genauso gut kaufen. Der Preis war letztendlich der selbe. :-) Fragst du dich nach der Moral von der Geschicht? Internetzugang ist heute wirklich nicht mehr teuer! Worauf ich aber hinaus wollte -- beim stöbern hab ich diesen netten E-Mail Footer wiederentdeckt: #!text - - __ _____________ _________ __ +-| _\| __ | _\_ _|-\_ _/ _|----------/\_\-+ !:| _-| __ | /:| |:::::| | (_::::::\|/::\/_/:! !:|__/|_||_|__\:|_|:::::|_|\___|::::(o o)::::::! +--------------------------------oOO-(_)-OOo---+ ... wohl zuviel Zeit gehabt damals. :-) …
Weiter lesen: Das Internet vergisst nie
- 02.04.08: Verknotete Telefonkabel Reloaded
-
Auszug: Vor [anderthalb Jahren](/weblog/verknotete-telefonkabel/) habe ich mich über mein neues Telefon, speziell das Telefonkabel gefreut. Hier noch einmal das Zitat: > Unglaublich, mein Tischtelefon ist nun schon über 3 Wochen alt und das Höhrerkabel ist immernoch keine verdrehte Wulst. :-) Genau 1 Jahr und 9 Monate ist das her. Und von dem Höhrerkabel ist nur noch ein wulstiges Elend über geblieben: Zum Vergleich, so sah es wirklich (!) damals aus: Nun währ das ja alles nicht so schlimm, so ein Spiralkabel kostet nur ein paar Euro und läßt sich geschwind auswechseln. Aber nicht bei mir... was habt ihr Elmegs euch eigentlich dabei gedacht? :-( Hier ist das Kabel fest im Höhrer verlötet. Danke Elmeg (Jetzt [Funkwerk](http://www.funkwerk-ec.com/)). …
Weiter lesen: Verknotete Telefonkabel Reloaded
- 01.04.08: Putbusser Park im Winter
-
Auszug: Nachdem es den ganzen Winter eigentlich kaum geschneit hat, gabs Ende März noch einmal etwa 5cm. Schnee. Dabei sind diese Bilder entstanden. Mehr Bilder folgen in den nächsten Tagen, wenn mein Windows resp. Lightroom wieder läuft. …
Weiter lesen: Putbusser Park im Winter
- 01.04.08: News des Tages
-
Auszug: ### Walter Freiwald übernimmt "ZDF Fernsehgarten" Juhu, jeder hätte es ahnen können. [Quelle](http://www.dwdl.de/article/news_15257,00.html) ### Sommerzeit steht vor dem Aus Die jährliche Umstellung auf die Sommerzeit wird vielleicht schon bald der Vergangenheit angehören! [Quelle](http://www.tagesschau.de/inland/zeitumstellung16.html) ### Bundesbank möchte die Goldreservern verkaufen Ich kann garnicht glauben, dass nicht zu glauben. Aber was kann man heute schon glauben... [Quelle](http://www.spiegel.de/wirtschaft/0,1518,544418,00.html) ### Klinsi nicht nach München Kahn ins Management, nix für Klinsi. Gut so! [Quelle](http://sport.t-online.de/c/14/65/77/44/14657744.html) …
Weiter lesen: News des Tages
- 30.03.08: Moo-Wahnsinn
-
Auszug: *martin@pixelbox:~$ apt-get moo* #!text (__) (oo) /------\/ / | || * /\---/\ ~~ ~~ ...."Have you mooed today?"... *martin@pixelbox:~$ aptitude moo* In diesem Programm gibt es keine Easter Eggs. *martin@pixelbox:~$ aptitude -v moo* In diesem Programm gibt es wirklich keine Easter Eggs. *martin@pixelbox:~$ aptitude -vv moo* Habe ich nicht bereits erklärt, dass es in diesem Programm keine Easter Eggs gibt? *martin@pixelbox:~$ aptitude -vvv moo* Hör auf! *martin@pixelbox:~$ aptitude -vvvv moo* Okay, wenn ich Dir ein Easter Egg gebe, wirst Du dann aufhören? *martin@pixelbox:~$ aptitude -vvvvv moo* Gut, Du hast gewonnen. #!text /----\ -------/ \ / \ / | -----------------/ --------\ ---------------------------------------------- *martin@pixelbox:~$ aptitude -vvvvvv moo* Was das ist? Natürlich ein Elefant, der von einer Schlange gefressen wurde. …
Weiter lesen: Moo-Wahnsinn