Remove file objects from children models on delete - python

My model File has as main purpose to link multiple files for one Invoice.
class File(models.Model):
invoice = models.ForeignKey(Invoice, related_name = 'files', on_delete = models.CASCADE)
file = models.FileField(upload_to = 'storage/invoicing/')
def delete(self, *args, **kwargs):
self.file.delete()
return super(File, self).delete(*args, **kwargs)
When i delete one instance of my model File, the file stored in storage/invoicing is also deleted because of my modified delete() method.
However, if i delete the instance from the parent model Invoice, the file is not deleted. Even with the File instance being removed from the database, the file is still acessable.
How can i code the parent model to delete everything from the children model, including the files?
I've searched a bit and i know that probably signals like post_delete can help me here, but i really don't know how to code it.

pre_delete signal can help you to solve the problem.
your_app/signals.py
from django.db.models.signals import pre_delete
from your_app.models import Invoice
def remove_files(instance, **kwargs):
for file_obj in instance.files.all():
file_obj.file.delete()
pre_delete.connect(remove_files, sender=Invoice)
IMPORTANT!
As you might have guessed, pre_delete signal calls the specified function before deleting the object(in this case instance of Invoice class). It means that before removing the invoice and all related file objects(because of CASCADE), it will delete all files associated with the file objects that refer to the invoice. If for some reason deletion won't happen you'll lose files anyway.

Related

File manipulation in Django models.py

I am building a Django app that saves an .stl file passed using a formulary and my goal is to open the file, extract some information with a script that is already tested, and save this information in the same register that the file.
I am doing this:
from stl import mesh # numpy-stl library
def informationGeneration(stl_route, *args, **kwargs):
# scripts that generates the information
myMesh = mesh.Mesh.from_file(stl_route) # here the error appears
return myMesh.areas.shape[0]
class Piece(models.Model):
"""Piece model."""
# ...
file = models.FileField(upload_to='pieces/files', default='NA')
# ...
information = models.IntegerField(default=0)
def save(self, *args, **kwargs):
"""Overriding the save method."""
self.information = informationGeneration(self.file)
super().save(*args, **kwargs)
def __str__(self):
# ...
The problem is that when I try to save a new instance, numpy-stl detects an error, self.file is not the .stl file, is an alement of the formulary.
Then, I use a form:
class PieceForm(forms.ModelForm):
"""Pieces model form."""
class Meta:
"""Form settings."""
model = Piece
fields = ('file')
How can I pass the file and not the route?
Piece.file is not a path, it's a models.FileField. To get the path, you have to use self.file.path.
Just beware that if there's actually no file for this field, self.file.path will raise an exception (ValueError, "the file attribute has no file associated with it"), so it's better to test before. models.FileField have a false value in a boolean context, so you want:
if self.file:
self.information = informationGeneration(self.file.path)
A couple notes:
1/ a function is an action, so it's name should be a verb (ie "extract_informations")
2/ you probably don't want to re-parse the file's content each and every time your object is saved, only when the file has changed. You can use a md5sum (stored in the model) to check this.
3/ I have not double-checked but I really dont think you should use a default for this field - if you want to make it optional, use blank=True and null=True.

Django admin bulk delete exclude certain queryset

I have a multiple objects of a File model.
I am trying filter and delete these files based on a certain condition but am failing to succeed in this.
Consider the following
I have 3 File objects:
File1
File2
File3
I tried to override the delete() function of the model like this:
def delete(self, using=None, keep_parents=False):
test_qs = File.objects.filter(file_name='File1')
if test_qs:
for x in test_qs:
x.delete()
super(File, self).delete()
When I go to my Django Admin, select all the files (File1, File2 & File3) and bulk delete them, all of them are deleted instead of just File1.
In my Django Console File.objects.filter(file_name='File1') returns a queryset with just File1.
I also tried to override the pre_delete signal like this:
#receiver(pre_delete, sender=File)
def delete_certain_files(sender, instance, **kwargs):
test_qs = File.objects.filter(file_name='File1')
test_qs.delete()
This however, results in a RecursionError
How do I make sure to just delete the File objects that meet a certain condition upon bulk deletion?
So, If you want this on admin. Imagine we have Foo model and FooAdmin class
class FooAdmin(admin.ModelAdmin):
actions = ['delete_selected']
def delete_selected(self, request, queryset):
# request: WSGIRrequest
# queryset: QuerySet, this is used for deletion
lookup_kwargs = {'pk__gt': 5000} # you can add your own condition.
queryset.filter(**lookup_kwargs)
admin.site.register(Foo, FooAdmin)

Django - access ManyToManyField right after object was saved

I need to notify users by email, when MyModel object is created. I need to let them know all attributes of this object including ManyToManyFields.
class MyModel(models.Model):
charfield = CharField(...)
manytomany = ManyToManyField('AnotherModel'....)
def to_email(self):
return self.charfield + '\n' + ','.join(self.manytomany.all())
def notify_users(self):
send_mail_to_all_users(message=self.to_email())
The first thing I tried was to override save function:
def save(self, **kwargs):
created = not bool(self.pk)
super(Dopyt, self).save(**kwargs)
if created:
self.notify_users()
Which doesn't work (manytomany appears to be empty QuerySet) probably because transaction haven't been commited yet.
So I tried post_save signal with same result - empty QuerySet.
I can't use m2mchanged signal because:
manytomany can be None
I need to notify users only if object was created, not when it's modified
Do you know how to solve this? Is there some elegant way?

How to save related model instances before the model instance in django?

How to save the related model instances before the instance model.
This is necessary because I want to preprocess the related model's instance field under model instance save method.
I am working on Django project, and I am in a situation, that I need to run some function, after all the related models of instance get saved in the database.
Let say I have a model
models.py
from . import signals
class Video(models.Model):
"""Video model"""
title = models.CharField(
max_length=255,
)
keywords = models.ManyToManyField(
KeyWord,
verbose_name=_("Keywords")
)
When the new instance of video model is created.
I need to
1. All the related models get saved first.
a. If the related models are empty return empty or None
2. and then Save this video instance.
I tried to do it using post_save signals, but couldn't succeed as there is no guarantee that related models get saved first that the model.
from django.db.models.signals import post_save, pre_delete, m2m_changed
from django.dispatch import receiver
from .models import Video
#receiver(m2m_changed, sender=Video)
#receiver(post_save, sender=Video)
def index_or_update_video(sender, instance, **kwargs):
"""Update or create an instance to search server."""
# TODO: use logging system
# Grab the id
print("Id is", instance.id)
# Keywords is empty as keyword instance is saved later than this instace.
keywords = [keyword.keyword for keyword in instance.keywords.all()]
print(keywords) # [] empty no keywords
instance.index()
#receiver(pre_delete, sender=Video)
def delete_video(sender, instance, **kwargs):
print("Delete index object")
instance.delete()
Update:
Can be implemented by grabbing the post_save signals and wait unitls
its related models get saved in db, when the related_models get saved
start serialization process and create flat json file along with the models fields and its related instance so, the flat json file can index
into elastic search server.
And the question aries, how much time should we wait in signal handler method? and how to know all instance related fields got saved in db.
class Video(models.Model):
def save(self, *args, **kwargs):
# 1. Make sure all of its related items are saved in db
# 2. Now save this instance in db.
# 3. If the model has been saved. Serialize its value,
# 4. Serailize its related models fields
# 5. Save all the serialized data into index server
# The advantage of using this is the data are indexed in real
# time to index server.
# I tired to to implement this logic using signals, in case of
# signals, when the instance get saved, its related models are
# not instantly available in the databse.
# Other solution could be, grab the `post_save` signals, wait(delay
# the serialization process) and start the serialization of
# instance model and it's related to convert the data to flat json
# file so, that it could index in the searching server(ES) in real
# time.
# until the instance related models get saved and start to
# serialize the data when its
By the way I am using django-admin and I am not defining the logics in
a view, adding the related model instance is handled by django admin
In this case, you can flip the order with which ModelAdmin calls save_model() and save_related() so from Model.save() you will be able to reach the updated values of the related fields, as stated in this post.
class Video(models.Model):
def save(self, *args, **kwargs):
if not self.id:
super().save(*args, **kwargs)
all_updated_keywards = self.keywards.all()
...
super().save(*args, **kwargs)
class VideoAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if not obj.pk:
super().save_model(request, obj, form, change)
else:
pass
def save_related(self, request, form, formsets, change):
form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=change)
super().save_model(request, form.instance, form, change)
You can override model's save() method and save related models (objects) before saving instance.

Replacing a Django image doesn't delete original

In Django, if you have a ImageFile in a model, deleting will remove the associated file from disk as well as removing the record from the database.
Shouldn't replacing an image also remove the unneeded file from disk? Instead, I see that it keeps the original and adds the replacement.
Now deleting the object won't delete the original file only the replacement.
Are there any good strategies to doing this? I don't want to have a bunch of orphan files if my users replace their images frequently.
The best strategy I've found is to make a custom save method in the model:
class Photo(models.Model):
image = ImageField(...) # works with FileField also
def save(self, *args, **kwargs):
# delete old file when replacing by updating the file
try:
this = Photo.objects.get(id=self.id)
if this.image != self.image:
this.image.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Photo, self).save(*args, **kwargs)
And beware, as with the updating which doesn't delete the back end file, deleting an instance model (here Photo) will not delete the back-end file, not in Django 1.3 anyway, you'll have to add more custom code to do that (or regularly do some dirty cron job).
Finally test all your update/delete cases with your ForeignKey, ManytoMany and others relations to check if the back-end files are correctly deleted. Believe only what you test.
Shouldn't replacing an image also remove the unneeded file from disk?
In the olden days, FileField was eager to clean up orphaned files. But that changed in Django 1.2:
In earlier Django versions, when a model instance containing a FileField was deleted, FileField took it upon itself to also delete the file from the backend storage. This opened the door to several potentially serious data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.2.5, FileField will never delete files from the backend storage.
The code in the following working example will, upon uploading an image in an ImageField, detect if a file with the same name exists, and in that case, delete that file before storing the new one.
It could easily be modified so that it deletes the old file regardless of the filename. But that's not what I wanted in my project.
Add the following class:
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
def _save(self, name, content):
if self.exists(name):
self.delete(name)
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
And use it with ImageField like so:
class MyModel(models.Model):
myfield = models.ImageField(
'description of purpose',
upload_to='folder_name',
storage=OverwriteStorage(), ### using OverwriteStorage here
max_length=500,
null=True,
blank=True,
height_field='height',
width_field='width'
)
height = models.IntegerField(blank=True, null=True)
width = models.IntegerField(blank=True, null=True)
If you don't use transactions or you don't afraid of loosing files on transaction rollback, you can use django-cleanup
There have been a number of tickets regarding this issue though it is likely this will not make it into the core. The most comprehensive is http://code.djangoproject.com/ticket/11663. The patches and ticket comments can give you some direction if you are looking for a solution.
You can also consider using a different StorageBackend such as the Overwrite File Storage System given by Django snippet 976. http://djangosnippets.org/snippets/976/. You can change your default storage to this backend or you can override it on each FileField/ImageField declaration.
Here is a code that can work with or without upload_to=... or blank=True, and when the submitted file has the same name as the old one.
(py3 syntax, tested on Django 1.7)
class Attachment(models.Model):
document = models.FileField(...) # or ImageField
def delete(self, *args, **kwargs):
self.document.delete(save=False)
super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
if self.pk:
old = self.__class__._default_manager.get(pk=self.pk)
if old.document.name and (not self.document._committed or not self.document.name):
old.document.delete(save=False)
super().save(*args, **kwargs)
Remember that this kind of solution is only applicable if you are in a non transactional context (no rollback, because the file is definitively lost)
I used a simple method with popen, so when i save my Info model i delete the former file before linking to the new:
import os
try:
os.popen("rm %s" % str(info.photo.path))
except:
#deal with error
pass
info.photo = nd['photo']
I save the original file and if it has changed - delete it.
class Document(models.Model):
document = FileField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._document = self.document
def save(self, *args, **kwargs):
if self.document != self._document:
self._document.delete()
super().save(*args, **kwargs)

Categories

Resources