Comment demander à Django Admin de supprimer des fichiers lorsque je supprime un objet de la base de données / du modèle?


85

J'utilise 1.2.5 avec un ImageField standard et j'utilise le backend de stockage intégré. Les fichiers sont bien téléchargés, mais lorsque je supprime une entrée de l'administrateur, le fichier réel sur le serveur n'est pas supprimé.


Hm, en fait ça devrait. Vérifiez les autorisations de fichier sur votre dossier de téléchargement (remplacez-le par 0777).
Torsten Engelbrecht

5
Django a supprimé la fonction de suppression automatique (pour les Googleurs qui voient le commentaire ci-dessus).
Marquez

Réponses:


100

Vous pouvez recevoir le signal pre_deleteou post_delete(voir le commentaire de @ toto_tico ci-dessous) et appeler la méthode delete () sur l'objet FileField, donc (dans 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)

10
Assurez-vous d'ajouter une vérification si le instance.filechamp n'est pas vide ou il peut (au moins essayer) de supprimer tout le répertoire MEDIA_ROOT. Cela s'applique même aux ImageField(null=False)champs.
Antony Hatchkins

47
Merci. En général, je recommanderais d'utiliser le post_deletesignal car il est plus sûr dans le cas où la suppression échoue pour une raison quelconque. Ensuite, ni le modèle, ni le fichier ne seraient supprimés en gardant les données cohérentes. Veuillez me corriger si ma compréhension post_deleteet mes pre_deletesignaux sont erronés.
toto_tico

9
Notez que cela ne supprime pas l'ancien fichier si vous remplacez le fichier sur une instance de modèle
Marquez

3
Cela ne fonctionne pas pour moi dans Django 1.8 en dehors de l'administration. Y a-t-il une nouvelle façon de le faire?
calife

impressionnant. recherchait cela depuis longtemps
RL Shyam

46

Essayez django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)

1
Package très génial. Merci! :)
BoJack Horseman

3
Après des tests limités, je peux confirmer que ce paquet fonctionne toujours pour Django 1.10.
CoderGuy123

1
Bien, c'est si facile
Tunn

Agréable. Fonctionne pour moi sur Django 2.0. J'utilise également S3 comme backend de stockage ( django-storages.readthedocs.io/en/latest/backends/… ) et je supprime volontiers les fichiers de S3.
routeburn

35

Solution Django 1.5: J'utilise post_delete pour diverses raisons internes à mon application.

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)

J'ai collé ceci au bas du fichier models.py.

le original_imagechamp est le ImageFielddans mon Photomodèle.


7
Pour toute personne utilisant Amazon S3 comme backend de stockage (via django-storages), cette réponse particulière ne fonctionnera pas. Vous obtiendrez un NotImplementedError: This backend doesn't support absolute paths.Vous pouvez facilement résoudre ce problème en passant le nom du champ de fichier à la storage.delete()place du chemin du champ de fichier. Par exemple, remplacez les deux dernières lignes de cette réponse par storage, name = photo.original_image.storage, photo.original_image.namealors storage.delete(name).
Sean Azlin

2
@Sean +1, j'utilise cet ajustement dans 1.7 pour supprimer les vignettes générées par django-imagekit sur S3 via django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Remarque: Si vous utilisez simplement un ImageField (ou FileField), vous pouvez l'utiliser à la mymodel.myimagefield.delete(save=False)place. docs.djangoproject.com/en/dev/ref/files/file/…
user2616836

@ user2616836 Pouvez - vous utiliser mymodel.myimagefield.delete(save=False)sur post_delete? En d'autres termes, je peux voir que je peux supprimer le fichier, mais pouvez-vous supprimer le fichier lorsqu'un modèle contenant le champ d'image est supprimé?
eugene

1
@eugene Oui, vous pouvez, cela fonctionne (je ne sais pas pourquoi). Dans post_deletevous instance.myimagefield.delete(save=False), notez l'utilisation de instance.
user2616836

17

Ce code fonctionne bien sur Django 1.4 également avec le panneau d'administration.

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)

Il est important d'obtenir le stockage et le chemin avant de supprimer le modèle, sinon ce dernier persistera également s'il est supprimé.


3
Cela ne fonctionne pas pour moi (Django 1.5) et le CHANGELOG de Django 1.3 déclare: "Dans Django 1.3, lorsqu'un modèle est supprimé, la méthode delete () de FileField ne sera pas appelée. Si vous avez besoin de nettoyer les fichiers orphelins, vous ' Vous devrez le gérer vous-même (par exemple, avec une commande de gestion personnalisée qui peut être exécutée manuellement ou programmée pour s’exécuter périodiquement via par exemple cron). »
darrinm

4
Cette solution est fausse! deleten'est pas toujours appelée lorsqu'une ligne est supprimée, vous devez utiliser des signaux.
lvella

11

Vous devez supprimer le fichier réel à la fois sur deleteet 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)

Excellente solution!
AlexKh

6

Vous pouvez envisager d'utiliser un signal pre_delete ou post_delete:

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

Bien entendu, les mêmes raisons pour lesquelles la suppression automatique de FileField a été supprimée s'appliquent également ici. Si vous supprimez un fichier référencé ailleurs, vous aurez des problèmes.

Dans mon cas, cela me semblait approprié car j'avais un modèle de fichier dédié pour gérer tous mes fichiers.

Remarque: pour une raison quelconque, post_delete ne semble pas fonctionner correctement. Le fichier a été supprimé, mais l'enregistrement de la base de données est resté, ce qui est complètement le contraire de ce à quoi je m'attendais, même dans des conditions d'erreur. pre_delete fonctionne bien cependant.


3
post_deletene fonctionnera probablement pas, car file_field.delete()par défaut enregistre le modèle dans la base de données, essayez file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields
Adam Jurczyk

3

C'est peut-être un peu tard. Mais le moyen le plus simple pour moi est d'utiliser un signal post_save. Rappelez-vous simplement que les signaux sont exécutés même pendant un processus de suppression QuerySet, mais que la méthode [model] .delete () n'est pas exécutée pendant le processus de suppression QuerySet, ce n'est donc pas la meilleure option pour la remplacer.

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)
"""--------------------------------------------------------------------------"""

core / signaux.py

import os

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

1

Cette fonctionnalité sera supprimée dans Django 1.3, donc je ne m'y fierai pas.

Vous pouvez remplacer la deleteméthode du modèle en question pour supprimer le fichier avant de supprimer complètement l'entrée de la base de données.

Éditer:

Voici un exemple rapide.

class MyModel(models.Model):

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

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

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

Avez-vous un exemple d'utilisation de cela dans un modèle afin de supprimer le fichier? Je regarde la documentation et vois des exemples de suppression de l'objet de la base de données mais je ne vois aucune implémentation lors de la suppression de fichiers.
narkeeso

2
Cette méthode est erronée car elle ne fonctionnera pas pour la suppression en bloc (comme la fonction `` Supprimer la sélection '' de l'administrateur). Par exemple MyModel.objects.all()[0].delete()supprimera le fichier alors que MyModel.objects.all().delete()ce ne sera pas le cas. Utilisez des signaux.
Antony Hatchkins

1

L'utilisation de post_delete est certainement la bonne façon de procéder. Parfois, les choses peuvent mal tourner et les fichiers ne sont pas supprimés. Il y a bien sûr le cas où vous avez un tas d'anciens fichiers qui n'ont pas été supprimés avant l'utilisation de post_delete. J'ai créé une fonction qui supprime les fichiers pour les objets en fonction du fait que le fichier auquel l'objet fait référence n'existe pas, puis supprime l'objet, si le fichier n'a pas d'objet, puis supprime également, il peut également supprimer en fonction d'un indicateur "actif" pour un objet .. Quelque chose que j'ai ajouté à la plupart de mes modèles. Vous devez lui transmettre les objets que vous souhaitez vérifier, le chemin d'accès aux fichiers objets, le champ fichier et un drapeau pour supprimer les objets inactifs:

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

Voici comment vous pouvez utiliser ceci:

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

0

assurez-vous que vous écrivez " self " avant le fichier. donc l'exemple ci-dessus devrait être

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

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

J'ai oublié le "soi" avant mon fichier et cela ne fonctionnait pas comme il le recherchait dans l'espace de noms global.



0

Solution Django 2.x:

Il n'est pas nécessaire d'installer de packages! C'est très facile à gérer dans Django 2 . J'ai essayé la solution suivante en utilisant Django 2 et le stockage SFTP (mais je pense que cela fonctionnerait avec tous les stockages)

Écrivez d'abord un gestionnaire personnalisé . Donc, si vous voulez pouvoir supprimer les fichiers d'un modèle à l'aide de objectsméthodes, vous devez écrire et utiliser un [Gestionnaire personnalisé] [3] (pour remplacer la delete()méthode de objects):

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

Maintenant, vous devez supprimer imageavant de supprimer la suppression du modèle lui-même et pour l'affecter CustomManagerau modèle, vous devez initialiser objectsdans votre modèle:

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()

-1

Je peux avoir un cas particulier puisque j'utilise l'option upload_to sur mon champ de fichier avec des noms de répertoire dynamiques mais la solution que j'ai trouvée était d'utiliser os.rmdir.

Dans les modèles:

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)

1
C'est une très mauvaise idée. Non seulement vous supprimerez un répertoire entier par rapport à un seul fichier (affectant potentiellement d'autres fichiers), mais vous le ferez même si la suppression de l'objet échoue.
tbm

Ce n'est pas une mauvaise idée si vous travailliez sur le problème que j'avais;) Comme je l'ai mentionné, j'ai eu un cas d'utilisation unique où le modèle supprimé était un modèle parent. Les enfants écrivaient des fichiers dans le dossier parent et donc si vous supprimiez le parent, le comportement souhaité était que tous les fichiers du dossier étaient supprimés. Bon point sur l'ordre des opérations cependant. Cela ne m'est pas venu à l'esprit à l'époque.
carruthd

Je préférerais toujours supprimer les fichiers enfants individuels lorsqu'un enfant est supprimé; puis si vous en avez besoin, vous pouvez supprimer le répertoire parent lorsqu'il est vide.
tbm

Cela a du sens lorsque vous supprimez des objets enfants, mais si l'objet parent est détruit, parcourir les enfants un à un semble fastidieux et inutile. Quoi qu'il en soit, je vois maintenant que la réponse que j'ai donnée n'était pas assez spécifique à la question du PO. Merci pour les commentaires, vous m'avez fait penser à utiliser un instrument moins émoussé à l'avenir.
carruthd
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.