Überschreiben hochgeladener Dateien
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 im Django-Repository aber es schaut nicht so aus, als ob es in Django 1.1 eingespielt wird.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 3 4 5 | # 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 geschrieben.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 hat ein Admin-Widget konstruiert, dass Dateien auf Klick wieder löscht.
Group By Funktionen in Querysets (Django 1.1+)
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:
1 | 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:
1 2 3 4 5 | 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:
1 2 3 | >>> 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 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-Funktion.
1 2 3 4 | >>> 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:
1 2 3 | class Entry(models.Model): created = models.DateTimeField() title = models.CharField(max_length=120) |
Füttern wir das Model wieder mit ein paar Testdaten:
1 2 3 | >>> 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:
1 2 3 4 | >>> 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:
1 | 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 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 Methode gedacht. Mit ihr kann man rohes SQL in den Query einsetzen. Folgendes Beispiel resultiert gleich:
1 2 | Queryset: Entry.objects.extra(select={'day': 'DATE(`created`)'})
SQL: SELECT *, DATE(`created`) AS day FROM entry_table
|
Zusammengefügt schaut unser Queryset jetzt so aus:
1 2 3 4 5 | >>> 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:
1 2 3 4 5 6 | >>> 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. ![]()