Wie bringe ich Django Admin dazu, Dateien zu löschen, wenn ich ein Objekt aus der Datenbank / dem Modell entferne?

84

Ich verwende 1.2.5 mit einem Standard-ImageField und verwende das integrierte Speicher-Backend. Das Hochladen von Dateien ist in Ordnung, aber wenn ich einen Eintrag vom Administrator entferne, wird die eigentliche Datei auf dem Server nicht gelöscht.

narkeeso
quelle
Hm, eigentlich sollte es. Überprüfen Sie die Dateiberechtigungen für Ihren Upload-Ordner (ändern Sie in 0777).
Torsten Engelbrecht
5
Django hat die automatische Löschfunktion entfernt (für Googler, die den obigen Kommentar sehen).
Mark

Antworten:

98

Sie können das Signal pre_deleteoder post_deleteempfangen (siehe den Kommentar von @ toto_tico unten) und die Methode delete () für das FileField-Objekt aufrufen, also (in models.py):

class MyModel(models.Model):
    file = models.FileField()
    ...

# Receive the pre_delete signal and delete the file associated with the model instance.
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver

@receiver(pre_delete, sender=MyModel)
def mymodel_delete(sender, instance, **kwargs):
    # Pass false so FileField doesn't save the model.
    instance.file.delete(False)
darrinm
quelle
10
Stellen Sie sicher, dass Sie eine Überprüfung hinzufügen, wenn das instance.fileFeld nicht leer ist, oder es kann (zumindest versuchen), das gesamte MEDIA_ROOT-Verzeichnis zu löschen. Dies gilt auch für ImageField(null=False)Felder.
Antony Hatchkins
46
Vielen Dank. Im Allgemeinen würde ich empfehlen, das post_deleteSignal zu verwenden, da es sicherer ist, wenn das Löschen aus irgendeinem Grund fehlschlägt. Dann würden weder das Modell noch die Datei gelöscht, um die Daten konsistent zu halten. Bitte korrigieren Sie mich, wenn mein Verständnis post_deleteund meine pre_deleteSignale falsch sind.
toto_tico
9
Beachten Sie, dass dies nicht die alte Datei nicht löschen , wenn Sie die Datei auf einer Modellinstanz ersetzen
Mark
3
Dies funktioniert bei mir in Django 1.8 außerhalb von admin nicht. Gibt es einen neuen Weg, dies zu tun?
Kalif
genial. suchte lange danach
RL Shyam
46

Versuchen Sie es mit Django-Aufräumarbeiten

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)
un1t
quelle
1
Sehr tolles Paket. Danke dir! :)
BoJack Horseman
3
Nach begrenzten Tests kann ich bestätigen, dass dieses Paket für Django 1.10 weiterhin funktioniert.
CoderGuy123
1
Schön, das ist so einfach
Tunn
Nett. Funktioniert für mich auf Django 2.0. Ich verwende S3 auch als Speicher-Backend ( django-storages.readthedocs.io/en/latest/backends/… ) und lösche gerne Dateien aus S3.
Routeburn
34

Django 1.5-Lösung: Ich verwende post_delete aus verschiedenen Gründen, die für meine App intern sind.

from django.db.models.signals import post_delete
from django.dispatch import receiver

@receiver(post_delete, sender=Photo)
def photo_post_delete_handler(sender, **kwargs):
    photo = kwargs['instance']
    storage, path = photo.original_image.storage, photo.original_image.path
    storage.delete(path)

Ich habe dies am Ende der Datei models.py festgehalten.

Das original_imageFeld ist das ImageFieldin meinem PhotoModell.

Kushal
quelle
6
Für alle, die Amazon S3 als Speicher-Backend verwenden (über Django-Speicher), funktioniert diese spezielle Antwort nicht. Sie erhalten eine NotImplementedError: This backend doesn't support absolute paths.Sie können dies leicht beheben, indem Sie den Namen storage.delete()des Dateifelds anstelle des Pfads des Dateifelds an übergeben. Ersetzen Sie beispielsweise die letzten beiden Zeilen dieser Antwort durch storage, name = photo.original_image.storage, photo.original_image.namedann storage.delete(name).
Sean Azlin
2
@ Sean +1, ich verwende diese Anpassung in 1.7, um Miniaturansichten zu löschen, die vom Django-Imagekit auf S3 über Django-Speicher generiert wurden. docs.djangoproject.com/de/dev/ref/files/storage/… . Hinweis: Wenn Sie einfach ein ImageField (oder FileField) verwenden, können Sie mymodel.myimagefield.delete(save=False)stattdessen verwenden. docs.djangoproject.com/de/dev/ref/files/file/…
user2616836
@ user2616836 Kannst du mymodel.myimagefield.delete(save=False)auf verwenden post_delete? Mit anderen Worten, ich kann sehen, dass ich die Datei löschen kann. Können Sie die Datei jedoch löschen, wenn ein Modell mit dem Bildfeld gelöscht wird?
Eugene
1
@ Eugene Ja du kannst, es funktioniert (ich bin mir nicht sicher warum). In post_deleteSie tun instance.myimagefield.delete(save=False), beachten Sie die Verwendung von instance.
user2616836
17

Dieser Code läuft auf Django 1.4 auch mit dem Admin-Panel gut.

class ImageModel(models.Model):
    image = ImageField(...)

    def delete(self, *args, **kwargs):
        # You have to prepare what you need before delete the model
        storage, path = self.image.storage, self.image.path
        # Delete the model before the file
        super(ImageModel, self).delete(*args, **kwargs)
        # Delete the file after the model
        storage.delete(path)

Es ist wichtig, den Speicher und den Pfad abzurufen, bevor das Modell gelöscht wird. Andernfalls bleibt das Modell auch dann ungültig, wenn es gelöscht wird.

Davide Muzzarelli
quelle
3
Dies funktioniert bei mir nicht (Django 1.5) und im Django 1.3 CHANGELOG heißt es: "In Django 1.3 wird beim Löschen eines Modells die delete () -Methode von FileField nicht aufgerufen. Wenn Sie verwaiste Dateien bereinigen müssen, müssen Sie ' Ich muss es selbst erledigen (zum Beispiel mit einem benutzerdefinierten Verwaltungsbefehl, der manuell ausgeführt oder so geplant werden kann, dass er regelmäßig über z. B. cron ausgeführt wird). "
Darrinm
4
Diese Lösung ist falsch! deleteWird nicht immer aufgerufen, wenn eine Zeile gelöscht wird, müssen Sie Signale verwenden.
lvella
10

Sie müssen die eigentliche Datei auf beiden deleteund entfernen update.

from django.db import models

class MyImageModel(models.Model):
    image = models.ImageField(upload_to='images')

    def remove_on_image_update(self):
        try:
            # is the object in the database yet?
            obj = MyImageModel.objects.get(id=self.id)
        except MyImageModel.DoesNotExist:
            # object is not in db, nothing to worry about
            return
        # is the save due to an update of the actual image file?
        if obj.image and self.image and obj.image != self.image:
            # delete the old image file from the storage in favor of the new file
            obj.image.delete()

    def delete(self, *args, **kwargs):
        # object is being removed from db, remove the file from storage first
        self.image.delete()
        return super(MyImageModel, self).delete(*args, **kwargs)

    def save(self, *args, **kwargs):
        # object is possibly being updated, if so, clean up.
        self.remove_on_image_update()
        return super(MyImageModel, self).save(*args, **kwargs)
un33k
quelle
Tolle Lösung!
AlexKh
6

Sie können ein Pre_delete- oder Post_delete-Signal verwenden:

https://docs.djangoproject.com/de/dev/topics/signals/

Die gleichen Gründe, aus denen das automatische Löschen von FileField entfernt wurde, gelten natürlich auch hier. Wenn Sie eine Datei löschen, auf die an einer anderen Stelle verwiesen wird, treten Probleme auf.

In meinem Fall schien dies angemessen zu sein, da ich ein dediziertes Dateimodell hatte, um alle meine Dateien zu verwalten.

Hinweis: Aus irgendeinem Grund scheint post_delete nicht richtig zu funktionieren. Die Datei wurde gelöscht, aber der Datenbankdatensatz blieb erhalten, was auch unter Fehlerbedingungen das Gegenteil von dem ist, was ich erwarten würde. pre_delete funktioniert jedoch einwandfrei.

SystemParadox
quelle
3
Wahrscheinlich post_deletefunktioniert es nicht, da das file_field.delete()Modell standardmäßig in der file_field.delete(False) Datenbank gespeichert
Adam Jurczyk,
3

Vielleicht ist es etwas spät. Am einfachsten ist es für mich jedoch, ein post_save-Signal zu verwenden. Nur um sich daran zu erinnern, dass Signale auch während eines QuerySet-Löschvorgangs ausgeführt werden, die Methode [model] .delete () jedoch während des QuerySet-Löschvorgangs nicht ausgeführt wird. Daher ist es nicht die beste Option, sie zu überschreiben.

core / models.py:

from django.db import models
from django.db.models.signals import post_delete
from core.signals import delete_image_slide
SLIDE1_IMGS = 'slide1_imgs/'

class Slide1(models.Model):
    title = models.CharField(max_length = 200)
    description = models.CharField(max_length = 200)
    image = models.ImageField(upload_to = SLIDE1_IMGS, null = True, blank = True)
    video_embed = models.TextField(null = True, blank = True)
    enabled = models.BooleanField(default = True)

"""---------------------------- SLIDE 1 -------------------------------------"""
post_delete.connect(delete_image_slide, Slide1)
"""--------------------------------------------------------------------------"""

Kern / Signale.py

import os

def delete_image_slide(sender, **kwargs):
    slide = kwargs.get('instance')
    try:
        os.remove(slide.image.path)
    except:
        pass
Mauricio
quelle
1

Diese Funktionalität wird in Django 1.3 entfernt, sodass ich mich nicht darauf verlassen würde.

Sie können die deleteMethode des betreffenden Modells überschreiben , um die Datei zu löschen, bevor Sie den Eintrag vollständig aus der Datenbank entfernen.

Bearbeiten:

Hier ist ein kurzes Beispiel.

class MyModel(models.Model):

    self.somefile = models.FileField(...)

    def delete(self, *args, **kwargs):
        somefile.delete()

        super(MyModel, self).delete(*args, **kwargs)
Derek Reynolds
quelle
Haben Sie ein Beispiel, wie Sie das in einem Modell verwenden können, um die Datei zu löschen? Ich schaue mir die Dokumente an und sehe Beispiele zum Entfernen des Objekts aus der Datenbank, sehe aber keine Implementierungen beim Löschen von Dateien.
Narkeeso
2
Diese Methode ist falsch, da sie beim Massenlöschen nicht funktioniert (wie die Funktion "Ausgewählte löschen" des Administrators). Zum Beispiel MyModel.objects.all()[0].delete()wird die Datei gelöscht, während dies MyModel.objects.all().delete()nicht der Fall ist. Verwenden Sie Signale.
Antony Hatchkins
1

Die Verwendung von post_delete ist mit Sicherheit der richtige Weg. Manchmal kann es jedoch zu Fehlern kommen und Dateien werden nicht gelöscht. Es gibt natürlich den Fall, dass Sie eine Reihe alter Dateien haben, die nicht gelöscht wurden, bevor post_delete verwendet wurde. Ich habe eine Funktion erstellt, die Dateien für Objekte basierend darauf löscht, ob die Datei, auf die das Objekt verweist, nicht vorhanden ist, dann Objekt löschen, wenn die Datei kein Objekt hat, dann auch löschen, kann sie auch basierend auf einem "aktiven" Flag für ein löschen Objekt .. Etwas, das ich den meisten meiner Modelle hinzugefügt habe. Sie müssen ihm die Objekte übergeben, die Sie überprüfen möchten, den Pfad zu den Objektdateien, das Dateifeld und ein Flag, um inaktive Objekte zu löschen:

def cleanup_model_objects(m_objects, model_path, file_field='image', clear_inactive=False):
    # PART 1 ------------------------- INVALID OBJECTS
    #Creates photo_file list based on photo path, takes all files there
    model_path_list = os.listdir(model_path)

    #Gets photo image path for each photo object
    model_files = list()
    invalid_files = list()
    valid_files = list()
    for obj in m_objects:

        exec("f = ntpath.basename(obj." + file_field + ".path)")  # select the appropriate file/image field

        model_files.append(f)  # Checks for valid and invalid objects (using file path)
        if f not in model_path_list:
            invalid_files.append(f)
            obj.delete()
        else:
            valid_files.append(f)

    print "Total objects", len(model_files)
    print "Valid objects:", len(valid_files)
    print "Objects without file deleted:", len(invalid_files)

    # PART 2 ------------------------- INVALID FILES
    print "Files in model file path:", len(model_path_list)

    #Checks for valid and invalid files
    invalid_files = list()
    valid_files = list()
    for f in model_path_list:
        if f not in model_files:
            invalid_files.append(f)
        else:
            valid_files.append(f)
    print "Valid files:", len(valid_files)
    print "Files without model object to delete:", len(invalid_files)

    for f in invalid_files:
        os.unlink(os.path.join(model_path, f))

    # PART 3 ------------------------- INACTIVE PHOTOS
    if clear_inactive:
        #inactive_photos = Photo.objects.filter(active=False)
        inactive_objects = m_objects.filter(active=False)
        print "Inactive Objects to Delete:", inactive_objects.count()
        for obj in inactive_objects:
            obj.delete()
    print "Done cleaning model."

So können Sie Folgendes verwenden:

photos = Photo.objects.all()
photos_path, tail = ntpath.split(photos[0].image.path)  # Gets dir of photos path, this may be different for you
print "Photos -------------->"
cleanup_model_objects(photos, photos_path, file_field='image', clear_inactive=False)  # image file is default
radtek
quelle
0

Stellen Sie sicher, dass Sie " self " vor der Datei schreiben . so sollte das obige Beispiel sein

def delete(self, *args, **kwargs):
        self.somefile.delete()

        super(MyModel, self).delete(*args, **kwargs)

Ich habe das "Selbst" vor meiner Datei vergessen und das hat nicht funktioniert, da es im globalen Namespace aussah.

Björn
quelle
0

Django 2.x Lösung:

Es müssen keine Pakete installiert werden! In Django 2 ist es sehr einfach zu handhaben . Ich habe versucht, die folgende Lösung mit Django 2 und SFTP-Speicher zu verwenden (ich denke jedoch, dass dies mit allen Speichern funktionieren würde).

Schreiben Sie zuerst einen benutzerdefinierten Manager . Wenn Sie also Dateien eines Modells mithilfe von objectsMethoden löschen möchten , müssen Sie einen [benutzerdefinierten Manager] [3] schreiben und verwenden (zum Überschreiben der delete()Methode von objects):

class CustomManager(models.Manager):
    def delete(self):
        for obj in self.get_queryset():
            obj.delete()

Jetzt müssen Sie löschen, imagebevor Sie das Modell selbst löschen. Um das Modell dem Modell zuzuweisen CustomManager, müssen Sie es objectsin Ihrem Modell initialisieren:

class MyModel(models.Model):
    image = models.ImageField(upload_to='/pictures/', blank=True)
    objects = CustomManager() # add CustomManager to model
    def delete(self, using=None, keep_parents=False):

    objects = CustomManager() # just add this line of code inside of your model

    def delete(self, using=None, keep_parents=False):
        self.image.storage.delete(self.song.name)
        super().delete()
Hamidreza
quelle
-1

Ich habe möglicherweise einen Sonderfall, da ich die Option upload_to in meinem Dateifeld mit dynamischen Verzeichnisnamen verwende, aber die Lösung, die ich gefunden habe, war die Verwendung von os.rmdir.

In Modellen:

import os

...

class Some_Model(models.Model):
     save_path = models.CharField(max_length=50)
     ...
     def delete(self, *args,**kwargs):
          os.rmdir(os.path.join(settings.MEDIA_ROOT, self.save_path)
          super(Some_Model,self).delete(*args, **kwargs)
Carruthd
quelle
1
Das ist eine sehr schlechte Idee. Sie entfernen nicht nur ein gesamtes Verzeichnis im Vergleich zu einer einzelnen Datei (was sich möglicherweise auf andere Dateien auswirkt), sondern auch dann, wenn das eigentliche Löschen des Objekts fehlschlägt.
TBB
Es ist keine schlechte Idee, wenn Sie an dem Problem arbeiten, das ich hatte;) Wie ich bereits erwähnte, hatte ich einen einzigartigen Anwendungsfall, bei dem das zu löschende Modell ein übergeordnetes Modell war. Kinder haben Dateien in den übergeordneten Ordner geschrieben. Wenn Sie also den übergeordneten Ordner gelöscht haben, war das gewünschte Verhalten, dass alle Dateien im Ordner gelöscht wurden. Guter Punkt in der Reihenfolge der Operationen. Das ist mir damals nicht eingefallen.
Carruthd
Ich würde es immer noch vorziehen, die einzelnen untergeordneten Dateien zu entfernen, wenn ein untergeordnetes Element gelöscht wird. Wenn nötig, können Sie das übergeordnete Verzeichnis entfernen, wenn es leer ist.
TBB
Dies ist sinnvoll, wenn Sie untergeordnete Objekte herausnehmen. Wenn das übergeordnete Objekt jedoch zerstört wird, erscheint es mühsam und unnötig, die untergeordneten Objekte einzeln zu durchlaufen. Unabhängig davon sehe ich jetzt, dass die Antwort, die ich gab, nicht spezifisch genug für die OP-Frage war. Vielen Dank für die Kommentare. Sie haben mich dazu gebracht, in Zukunft ein weniger stumpfes Instrument zu verwenden.
Carruthd