Sprachabhängige Template-Imports

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:

1
2
3
4
5
{% ifequal LANGUAGE_CODE "de" %}
    <p>Hier ganz viel deutscher Text.</p>
{% else %}
    <p>Here is a lot of english text, the default language.</p>
{% endif %}

Wird der Inhalt zu lang, kann man die Texte jeweils in eigene Templates auslagern und mittels include importieren:

1
2
3
4
5
{% 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 <templatename>.<languagecode> als Dateinamen besitzen. Angenommen unser User ist mit deutscher Spracheinstellung unterwegs und sein LANGUAGE_CODE wäre de, der Aufruf:

1
{% 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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.

Tägliche Postfix-Helfer

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!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
 0   6   *   *   *    /usr/sbin/pflogsumm -d yesterday /var/log/mail.info /var/log/mail.info.0 | mail -s "Mailstatistiken" postmaster@localhost

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 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.

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:

1
2
3
4
5
6
7
# 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.

Variable Spamchecks mit Postfix trotz content_filter

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:

1
2
3
4
5
6
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, 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:

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

1
2
3
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 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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# --------------------------------------------------------------------
# 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

Plesk 8.x und 9.x: Keine Rechte zum Datenbank-Anlegen trotz Admin-Account

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.

Ordnung im Medienordner: Dynamische Upload-Pfade

Djangos FileField und das davon abgeleitete ImageField Feld enthalten ein upload_to-Argument das angibt, wohin die Datei im statischen Medienordner gespeichert werden soll:

1
2
3
4
5
6
7
8
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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. Smiley:  :) 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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. Smiley:  :)

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 verwerten kann. Ein upload_to='/images/%Y/%m/' würde das Bild im Ordner /images/2009/02/bild.jpg speichern.