resizing an image only if upload_to was called django - python

I have the following code, models.py:
class Location(models.Model):
image_file = models.ImageField(upload_to=upload_to_location, null=True, blank=True)
name = models.CharField(max_length=200)
which calls the upload_to function where the image is being renamed. Then my save method
def save(self, *args, **kwargs):
try:
this = Location.objects.get(id=self.id)
if this.image_file:
os.remove(this.image_file.path)
except ObjectDoesNotExist:
pass
super(Location, self).save(*args, **kwargs)
try:
this = Location.objects.get(id=self.id)
if this.image_file:
resize(this.image_file.path)
except ObjectDoesNotExist:
pass
def upload_to_location(instance, filename):
blocks = filename.split('.')
ext = blocks[-1]
filename = "%s.%s" % (instance.name.replace(" ", "-"), ext)
return filename
checks if a previously uploaded file exists and if so, it deletes it. And then after is being saved, it checks again if the file exists and then calls the resize function. That works, if I upload a file and click save. However, if I only change the name of the Location, the os.remove function is called, deletes my file and then the resize function trhows an error.
Therefore I only want to call the os.remove function when the user uploads a upload_to will be called.
So I tried a couple of things but couldn't get it done. I also tried pre_save and post_save signales. Hereby, I had a problem of getting the path of the image:
#receiver(pre_save, sender=Company)
def my_function2(sender, instance, **kwargs):
print instance.path #didn't work
print os.path.realpath(instance) #didn't work
Does anyone know how I could solve this either with pre_save or something else?

If you just want to call os.remove only when upload_to is invoked, why not move that code inside upload_to_location function?
def upload_to_location(self, filename):
try:
this = Location.objects.get(id=self.id)
if this.image_file:
os.remove(this.image_file.path)
except ObjectDoesNotExist:
pass
# do something else...
This way, when upload_to is called, os.remove will be called.
Extras
Instead of calling os.remove to delete associated file, you can delete the file by calling that file field's delete() method.
if this.image_file:
this.image_file.delete(save=True)
save=True saves the this instance after deleting the file. If you don't want that, pass save=False. Default is True.
And in the function my_function2 where you're listening to signals, it should be:
instance.image_file.path

Related

Getting the path of an uploaded media file that uses "upload_to" in django

I am trying to replace the images that will be uploaded for a certain ImageField in my models. Now My models are like this:
class Image(models.Model):
image = models.ImageField(upload_to='images/')
def save(self, *args, **kwargs):
# some resizing here which works fine
As you can see, my model saves the file to the 'images/' directory (because i have different image types that need to go in different sub-directories of /media) which will eventually become '/media/images/' due to this setting in my settings.py:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Now the problem is where I'm using the receiver method to delete the previously uploaded image.
#receiver(pre_save, sender=Image)
def file_update(sender, **kwargs):
instance = kwargs['instance']
print(instance.image.path) ### HERE IS THE PROBLEM
if instance.image:
path = instance.image.path ### HERE IS THE PROBLEM
os.remove(path)
It's supposed to return this:
/home/.../media/images/file.jpg
But it returns this:
/home/.../media/file.jpg
Which obviously results in a No such file or directory error. What am I missing?
A simple hack would be to do something like this:
path = instance.image.path
name = instance.image.name
correct_path = path.replace(name, 'images/' + name)
But that doesn't answer the question of why it happened or what's the correct way of doing it.
UPDATE:
In case someone is having the same problem, I tried another approach. First, I fetch the object with it's id and then get that path:
instance = kwargs['instance']
if instance.id is not None:
current_image = Image.objects.filter(id=instance.id)[0]
print(current_image.image.path) ### WORKS FINE
os.remove(current_image.image.path) ### WORKS FINE
This approach has two benefits:
The path will be correct.
Replacing the image is guaranteed to work because we are getting the path of previously saved object not calculating based on the newly submitted object (which if not existing, can lead to problems of not having an ID yet).
The only downside is an extra database query.
As Abhyudai indicated: Your 'problem' only occurs in the pre_save. In the post_save you are getting the correct path. I suspect that the correct 'path' is only generated when the file is actually saved (before saving the model it has not been written to the final destination.
However, you can still access the required data in your pre_save:
Absolute path of your media dir:
instance.image.storage.base_location # /home/.../media
The relative path of your upload_to:
instance.image.field.upload_to # images/
Note the .field as we want the field and not the FieldFile normally returned
And of course the name of your file
instance.image.name
Joining all these will give you the path you need to check and delete the existing file, if required
#receiver(pre_save, sender=Image)
def attachment_image_update(sender, **kwargs):
attachment = kwargs['instance']
if attachment.id:
attachment = Image.objects.get(pk=attachment.id)
try:
storage, path = attachment.image.storage, attachment.image.path
storage.delete(path)
except ValueError:
print('Error! The image does not exist')
or
#receiver(pre_save, sender=Image)
def attachment_image_update(sender, **kwargs):
attachment = kwargs['instance']
if attachment.id:
attachment = Image.objects.get(pk=attachment.id)
if attachment.image:
storage, path = attachment.image.storage, attachment.image.path
storage.delete(path)

Catching bulk events in Django

I have Book model. It has some fields like title, year publish and etc. Also, i have overrided save() method. When new books is adding, it checks if book exsists, it creating new directory with name self.title in MEDIA_ROOT path.
def save(self, *args, **kwargs):
book_dir = os.path.join(MEDIA_ROOT, self.title)
# check that at least one file is loading
if all([self.pdf, self.fb2, self.epub]):
raise ValidationError("At least 1 file should be uploaded!")
# create book's directory if it not exists
if os.path.exists(book_dir):
raise ValidationError("This book is already exists!")
else:
os.mkdir(book_dir)
# rename and edit storage location of books to book_dir
for field in [self.image, self.pdf, self.fb2, self.epub]:
field.storage.location = book_dir
super().save(*args, **kwargs) # Call the "real" save() method.
Also i have overrided delete() method, that just remove directory of deleted book.
def delete(self, *args, **kwargs):
book_dir = os.path.join(MEDIA_ROOT, self.title)
rmtree(book_dir)
super().delete(*args, **kwargs) # Call the "real" delete() method.
delete() method works well if i delete only 1 book.
But, if i want to delete multiple files (all actions take place in the admin panel), only DB records got deleted.
So, i want to just catch this moment to remove directory of deleted book.
Looks like pre_delete signal could be useful here: https://docs.djangoproject.com/en/2.2/ref/signals/#pre-delete

Why Django generate new migrations file each time I use `makemigrations` command. For ImageFIeld with changed upload_path attribute

I need to get ImageField name in upload_path function.
I tried use partial in ImageField definition:
class MyModel(models.Model):
image = models.ImageField(
upload_to=partial(image_upload_path, 'image')
)
Now I can get that string by first argument of function:
def image_upload_path(field, instance, filename):
....
All works fine, but now Django generate migration file, each time I use makemigrations, with same operations list in it:
operations = [
migrations.AlterField(
model_name='genericimage',
name='image',
field=core_apps.generic_image.fields.SorlImageField(upload_to=functools.partial(core_apps.generic_image.path.image_upload_path, *('image',), **{}),),
),
]
Maybe there is another way to access Field name in upload_path function or somehow I can fix my solution?
It seems like you don't need to provide a partial in this case, but just a callable with two parameters like in this example in the Django documentation.
Django will invoke the callable you provide in the upload_to argument with 2 parameters (instance and filename).
instance:
An instance of the model where the FileField is defined. More specifically, this is the particular instance where the current file is being attached.
This means you can access the name field of the instance like instance.name in the callable you write:
class MyModel(models.Model):
name = models.CharField(max_length=255)
image = models.ImageField(upload_to=image_upload_path)
def image_upload_path(instance, filename):
# Access the value of the `name` field
# of the MyModel instance passed in and save it to a variable:
name = instance.name
# Code that returns a Unix-style path (with forward slashes) goes here
I decide to build my own field:
class SorlImageField(ImageField):
def __init__(self, verbose_name=None, name=None, width_field=None,
height_field=None, lookup_name=None, **kwargs):
self.lookup_name = lookup_name
kwargs['upload_to'] = partial(image_upload_path, lookup_name)
super(SorlImageField, self).__init__(verbose_name, name,
width_field, height_field, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(SorlImageField, self).deconstruct()
del kwargs['upload_to']
# del upload_to will solve migration issue
return name, path, args, kwargs
def check(self, **kwargs):
errors = super(SorlImageField, self).check(**kwargs)
if self.lookup_name != self.name:
error = [
checks.Error(
'SorlImageField lookup_name must be equal to '
'field name, now it is: "{}"'.format(self.lookup_name),
hint='Add lookup_name in SorlImageField',
obj=self,
id='fields.E210',
)]
errors.extend(error)
return errors
Problem with migration was solved in deconstruct method, by deleting upload_to argument. Also I add additional argument into __init__ which point to field name, check function check for correct lookup_name value. If it not, it will raise an error when migrations starts.
class MyModel(models.Model):
image = SorlImageField(
lookup_name='image'
)

Django: Save a FileField before calling super()

I need to save an uploaded file before super() method is called. It should be saved, because i use some external utils for converting a file to a needed internal format. The code below produce an error while uploading file '123':
OSError: [Errno 36] File name too long: '/var/www/prj/venv/converted/usermedia/-1/uploads/123_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1_...'
It seems, that it tries to save it in super().save() twice with the same name in an infinite loop. Also, it creates all these files.
def save(self, **kwargs):
uid = kwargs.pop('uid', -1)
for field in self._meta.fields:
if hasattr(field, 'upload_to'):
field.upload_to = '%s/uploads' % uid
if self.translation_file:
self.translation_file.save(self.translation_file.name, self.translation_file)
#self.mimetype = self.guess_mimetype()
#self.handle_file(self.translation_file.path)
super(Resource, self).save(**kwargs)
EDIT:
Here is inelegant way i wanted to get around (it will double call save() method):
def save(self, *args, **kwargs):
uid = kwargs.pop('uid', -1)
for field in self._meta.fields:
if hasattr(field, 'upload_to'):
field.upload_to = '%s/uploads' % uid
super(Resource, self).save(*args, **kwargs)
if self.__orig_translation_file != self.translation_file:
self.update_mimetype()
super(Resource, self).save(*args, **kwargs)
You got an infinite loop in your first example, thats right.
Calling self.translation_file.save(self.translation_file.name, self.translation_file) will save the uploaded file to disk and call the Resources class save method again because the methods save paramter defaults to true (have a look here https://docs.djangoproject.com/en/dev/ref/files/file/#additional-methods-on-files-attached-to-objects) as well as your custom FileField does anyway.
Calling it like this (just add save=False) is more likely to work:
self.translation_file.save(self.translation_file.name, self.translation_file, save = False)
I hope this points into the right direction.

ImageField won't delete file when using os.unlink()

I have this model:
class UserProfile(models.Model):
"""(UserProfile description)"""
user = models.ForeignKey(User)
image = models.ImageField(upload_to=upload_to)
def save(self):
os.unlink(self.image.path)
super(UserProfile, self).save()
The script fails on the unlink() method, and says the file cannot be found. Any ideas?
The error says
(2, 'No such file or directory')
You need more specific debugging. Furthermore, the way you've written the code probably ensures errors will happen. I'm not entirely sure, but I wouldn't be surprised if the value for UserProfile.image isn't set before a UserProfile record is created.
So I would rewrite your code thusly:
class UserProfile(models.Model):
user = models.ForeignKey(User)
image = models.ImageField(upload_to=upload_to)
def save(self):
if self.image.path:
try:
os.unlink(self.image.path)
except Exception, inst:
raise Exception("Unable to delete %s. Error: %s" % (self.image.path, inst))
super(UserProfile, self).save()
Update
Now that I think about it, it does make sense that once you're calling save, the new information is already in self.image. If your goal is to delete the old images when saving the new, you are going to need to use the pre_save signal in Django to retrieve the old record (and thus the old image path) prior to saving the image. In theory, you could put this in the save method of the UserProfile model, but since it's intended as a side action that does not directly affect the current record, I'd keep it separate.
Here's a sample implementation:
from django.db.models import signals
def delete_old_image(sender, instance, using=None):
try:
old_record = sender.objects.get(pk=instance.pk)
os.unlink(old_record.image.path)
except sender.DoesNotExist:
pass
signals.pre_save.connect(delete_old_image, sender=UserProfile)
You would put this in your models.py file.

Categories