Django correctly delete image after updating file - django

There is a similar question here but that is from 2011 and Django 1.2. Many things have changed since then. I'm using Django 1.9.
I'm wondering how should I do this correctly, without any risks of messing up the filesystem. I have an ImageField in a model:
class CartaMagicPy(models.Model):
imagen = models.ImageField(null=True, upload_to=ubicar_magicpy)
On a first moment, the image is saved. But then, the user crops the image and I get a new image assigned to the same ImageField.
The old image is not deleted from the filesystem. How can I delete it? The problem is that model_object.imagen.path after being updated contains the path to the new image. And if I delete it before I update it with the new image, I risk that maybe the save could fail and then I end up with no image at all. Is there a combination of post-save signals I could use? Any advice will help.

As seen in this answer, "there's an app for that": django-cleanup.
If you prefer to implement it yourself, the accepted answer to the question #raphv linked in the comment gives some good pointers.
A good solution for updated change followup is to use model signals; I tend to use post_init and post_save, in this case to delete the old image file after the change has been made. Something like so:
from django.db.models.signals import post_init, post_save
from django.dispatch import receiver
from myapp.models import CartaMagicPy
#receiver(post_init, sender= CartaMagicPy)
def backup_image_path(sender, instance, **kwargs):
instance._current_imagen_file = instance.imagen
#receiver(post_save, sender= CartaMagicPy)
def delete_old_image(sender, instance, **kwargs):
if hasattr(instance, '_current_imagen_file'):
if instance._current_imagen_file != instance.imagen.path:
instance._current_imagen_file.delete(save=False)
This also seems to be generally what the linked package above does, plus some additional safety checks and cleanup.

We could optimize if the image accepts null values with the following code.
#receiver(post_init, sender= CartaMagicPy)
def backup_image_path(sender, instance, **kwargs):
instance._current_imagen_file = instance.imagen
#receiver(post_save, sender=CartaMagicPy)
def delete_old_image(sender, instance, **kwargs):
if hasattr(instance, '_current_imagen_file'):
if instance.imagen:
if instance._current_imagen_file != instance.imagen.path:
instance._current_imagen_file.delete(save=False)
else:
if instance._current_imagen_file:
instance._current_imagen_file.delete()

I am not sure, but you can try this
use Django-cleanup
pip install django-cleanup
settings.py
INSTALLED_APPS = (
...
'django_cleanup', # should go after your apps
...
)

Related

How to fix circular importing?

It seems I have a circular importing error. I currently just struggling to fix it. Does anyone know what I should do?
In my models.py, containing ReservedItems & Order:
def reserveditem_pre_save_receiver(sender, instance, **kwargs):
if not instance.order_reference:
instance.order_reference = unique_order_reference_generator()
In my utils.py
from lumis.utils import get_random_string
from .models import Order, ReservedItem
def unique_order_reference_generator():
new_id = get_random_string(length=10)
reserved_item = ReservedItem.objects.filter(
order_reference=new_id
).exists()
order = Order.objects.filter(order_reference=new_id).exists()
if reserved_item or order:
return unique_order_reference_generator()
else:
return new_id
You can import modules locally in the body of the function, so:
from lumis.utils import get_random_string
def unique_order_reference_generator():
from .models import Order, ReservedItem
new_id = get_random_string(length=10)
reserved_item = ReservedItem.objects.filter(
order_reference=new_id
).exists()
order = Order.objects.filter(order_reference=new_id).exists()
if reserved_item or order:
return unique_order_reference_generator()
else:
return new_id
This thus means that the module is not loaded when Python loads the file, but when the function is actually called. As a result, we can load the unique_order_reference_generator function, without having to load a the module that actually depends on this function.
Note that, like #Alasdair says, signals are typically defined in a dedicated file (signals.py) for example which should be loaded in the ready() function of the app. But regardless how you structure code, frequently local imports should be used to avoid circular imports.
All the current suggestions are good. Move your signal handlers out of models. Models are prone to circular imports because they are used everywhere, so it is a good idea to keep only model code in models.py.
Personally, I don't like imports in the middle of the code
import-outside-toplevel / Import outside toplevel
Instead I use Django application API to load models without importing
from django.apps import apps
def signal_handler(instance, *args, **kwargs):
Order = apps.get_model('your_app', 'Order')
...

Django signal m2m_changed not triggered

I recently started to use signals in my Django project (v. 1.3) and they all work fine except that
I just can't figure out why the m2m_changed signal never gets triggered on my model. The Section instance is edited by adding/deleting PageChild inline instances on an django admin form.
I tried to register the callback function either way as described in the documentation, but don't get any result.
Excerpt from my models.py
from django.db import models
from django.db.models.signals import m2m_changed
class Section(models.Model):
name = models.CharField(unique = True, max_length = 100)
pages = models.ManyToManyField(Page, through = 'PageChild')
class PageChild(models.Model):
section = models.ForeignKey(Section)
page = models.ForeignKey(Page, limit_choices_to = Q(is_template = False, is_background = False))
#receiver(m2m_changed, sender = Section.pages.through)
def m2m(sender, **kwargs):
print "m2m changed!"
m2m_changed.connect(m2m, sender = Section.pages.through, dispatch_uid = 'foo', weak = False)
Am I missing something obvious?
This is an open bug: https://code.djangoproject.com/ticket/16073
I wasted hours on it this week.
You are connecting it twice, once with m2m_changed.connect and the other time with receiver decorator.
Not sure if it will help, but the following is working for me:
class Flow(models.Model):
datalist = models.ManyToManyField(Data)
from django.db.models.signals import post_save, pre_delete, m2m_changed
def handle_flow(sender, instance, *args, **kwargs):
logger.debug("Signal catched !")
m2m_changed.connect(handle_flow, sender=Flow.datalist.through)
I'm not sure if this will help, but are you sure that you should use Sender.pages.through for this special case? perhaps if you tried #reciever(m2m_changed, sender=PageChild)
Note: if you have #reciever, you do not need m2_changed.connect(...) as #reciever already performs the connect operation.

How to use PIL in django models

I like to resize an uploaded image(ImageField) before finally storing, I heard that python has an image library called PIL and I would like to use it do that task but I'm not sure on how to start.
Any suggestions on how to do it?
Thanks
you can override model's save function where you can open file and resize it (not recommended, as it will resize it each time you save a model), you an resize after file upload (e.g. before/during form.save())
but IMHO a far better solution is to use a dedicated app for this, my favourite is sorl-thumbnails
I've just found out on how to do it but is there a way to simplify it? I'm new in python and django so I'm not sure if this is the proper way to do it.
below is my code:
from django.db.models.signals import pre_delete, pre_save
def on_save_image(sender, **kwargs):
import PIL
obj = kwargs['instance']
if obj.file:
try:
original = sender.objects.get(pk = obj.pk)
if original.file:
#if change image then delete original file
original.file.delete()
except ObjectDoesNotExist:
pass
finally:
img = PIL.Image.open(obj.file)
img.thumbnail((500, 500))
# reset pointer to start at 0 again
obj.file.open()
img.save(obj.file)
pre_save.connect(on_save_image, sender = Image)
# delete file in memory
def on_delete_image(sender, **kwargs):
obj = kwargs['instance']
if obj.file:
obj.file.delete()
pre_delete.connect(on_delete_image, sender = Image)
Thanks

Django with Pluggable MongoDB Storage troubles

I'm trying to use django, and mongoengine to provide the storage backend only with GridFS. I still have a MySQL database.
I'm running into a strange (to me) error when I'm deleting from the django admin and am wondering if I am doing something incorrectly.
my code looks like this:
# settings.py
from mongoengine import connect
connect("mongo_storage")
# models.py
from mongoengine.django.storage import GridFSStorage
class MyFile(models.Model):
name = models.CharField(max_length=50)
content = models.FileField(upload_to="appsfiles", storage=GridFSStorage())
creation_time = models.DateTimeField(auto_now_add=True)
last_update_time = models.DateTimeField(auto_now=True)
I am able to upload files just fine, but when I delete them, something seems to break and the mongo database seems to get in an unworkable state until I manually delete all FileDocument.objects. When this happens I can't upload files or delete them from the django interface.
From the stack trace I have:
/home/projects/vector/src/mongoengine/django/storage.py in _get_doc_with_name
doc = [d for d in docs if getattr(d, self.field).name == name] ...
▼ Local vars
Variable Value
_[1]
[]
d
docs
Error in formatting: cannot set options after executing query
name
u'testfile.pdf'
self
/home/projects/vector/src/mongoengine/fields.py in __getattr__
raise AttributeError
Am I using this feature incorrectly?
UPDATE:
thanks to #zeekay's answer I was able to get a working gridfs storage plugin to work. I ended up not using mongoengine at all. I put my adapted solution on github. There is a clear sample project showing how to use it. I also uploaded the project to pypi.
Another Update:
I'd highly recommend the django-storages project. It has lots of storage backed options and is used by many more people than my original proposed solution.
I think you are better off not using MongoEngine for this, I haven't had much luck with it either. Here is a drop-in replacement for mongoengine.django.storage.GridFSStorage, which works with the admin.
from django.core.files.storage import Storage
from django.conf import settings
from pymongo import Connection
from gridfs import GridFS
class GridFSStorage(Storage):
def __init__(self, host='localhost', port=27017, collection='fs'):
for s in ('host', 'port', 'collection'):
name = 'GRIDFS_' + s.upper()
if hasattr(settings, name):
setattr(self, s, getattr(settings, name))
for s, v in zip(('host', 'port', 'collection'), (host, port, collection)):
if v:
setattr(self, s, v)
self.db = Connection(host=self.host, port=self.port)[self.collection]
self.fs = GridFS(self.db)
def _save(self, name, content):
self.fs.put(content, filename=name)
return name
def _open(self, name, *args, **kwars):
return self.fs.get_last_version(filename=name)
def delete(self, name):
oid = fs.get_last_version(filename=name)._id
self.fs.delete(oid)
def exists(self, name):
return self.fs.exists({'filename': name})
def size(self, name):
return self.fs.get_last_version(filename=name).length
GRIDFS_HOST, GRIDFS_PORT and GRIDFS_COLLECTION can be defined in your settings or passed as host, port, collection keyword arguments to GridFSStorage in your model's FileField.
I referred to Django's custom storage documenation, and loosely followed this answer to a similar question.

django-photologue upload_to

I have been playing around with django-photologue for a while, and find this a great alternative to all other image handlings apps out there.
One thing though, I also use django-cumulus to push my uploads to my CDN instead of running it on my local machine / server.
When I used imagekit, I could always pass a upload_to='whatever' but I cannot seem to do this with photologue as it automatically inserts the imagefield. How would I go about achieving some sort of an overwrite?
Regards
I think you can hook into the pre_save signal of the Photo model, and change the upload_to field, just before the instance is saved to the database.
Take a look at this:
http://docs.djangoproject.com/en/dev/topics/signals/
Managed to find a workaround for it, however this requires you to make the changes in photologue/models.py
if PHOTOLOGUE_PATH is not None:
if callable(PHOTOLOGUE_PATH):
get_storage_path = PHOTOLOGUE_PATH
else:
parts = PHOTOLOGUE_PATH.split('.')
module_name = '.'.join(parts[:-1])
module = __import__(module_name)
get_storage_path = getattr(module, parts[-1])
else:
def get_storage_path(instance, filename):
dirname = 'photos'
if hasattr(instance, 'upload_to'):
if callable(instance.upload_to):
dirname = instance.upload_to()
else: dirname = instance.upload_to
return os.path.join(PHOTOLOGUE_DIR,
os.path.normpath( force_unicode(datetime.now().strftime(smart_str(dirname))) ),
filename)
And then in your apps models do the following:
class MyModel(ImageModel):
text = ...
name = ...
def upload_to(self):
return 'yourdirectorynamehere'
You can use the setting PHOTOLOGUE_PATH to provide your own callable. Define a method which takes instance and filename as parameters then return whatever you want. For example in your settings.py:
import photologue
...
def PHOTOLOGUE_PATH(instance, filename):
folder = 'myphotos' # Add your logic here
return os.path.join(photologue.models.PHOTOLOGUE_DIR, folder, filename)
Presumably (although I've not tested this) you could find out more about the Photo instance (e.g. what other instances it relates to) and choose the folder accordingly.