Djangos FileField und das davon abgeleitete ImageField Feld enthalten ein upload_to-Argument das angibt, wohin die Datei im statischen Medienordner gespeichert werden soll:
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:
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:
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:
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.