0

For a django project I have a model that features an image field. The idea is that the user uploads an image and django renames it according to a chosen pattern before storing it in the media folder.

To achieve that I have created a helper class in a separate file utils.py. The class will callable within the upload_to parameter of the django ImageField model. Images should be renamed by concatenating .name and .id properties of the created item. The problem with my solution is that if the image is uploaded upon creating anew the item object, then there is no .id value to use (just yet). So instead of having let's say: banana_12.jpg I get banana_None.jpg. If instead I upload an image on an already existing item object, then the image is renamed correctly. Here is my solution. How can I improve the code to make it work on new item object too? Is there a better way to do this?

# utils.py

from django.utils.deconstruct import deconstructible
import os

@deconstructible
class RenameImage(object):
    """Renames a given image according to pattern"""

    def __call__(self, instance, filename):
        """Sets the name of an uploaded image"""
        self.name, self.extension = os.path.splitext(filename)
        self.folder = instance.__class__.__name__.lower()
        
        image_name = f"{instance}_{instance.id}{self.extension}"

        return os.path.join(self.folder, image_name)

rename_image = RenameImage()
# models.py

from .utils import rename_image

class Item(models.Model):
    # my model for the Item object
    name = models.CharField(max_length=30)
    info = models.CharField(max_length=250, blank=True)
    image = models.ImageField(upload_to=rename_image, blank=True, null=True) # <-- helper called here
    # ...

I created the helper class RenameImage which for the reasons explained above works correctly only if the object already exists.

__EDIT

I have made some improvements based on the post_save advice. The user uploaded image gets resized and renamed as I want but then I get a RecursionError: maximum recursion depth exceeded. It seems that by calling the save() method on the instance I get into a signal loop... Any idea on how to resolve this?

from django.db.models.signals import post_save

from .models import Item
from PIL import Image
import os


def resize_rename(sender, instance, created, max_height=300, max_width=300, **kwargs):

    if instance.image: # only fire if there is an image
        with open(instance.image.path, 'r+b') as f:
            image = Image.open(f)
            
        # Resize the image if larger than max values
            if image.height > max_height or image.width > max_width:
                output_size = (max_height, max_width)
                image.thumbnail(output_size, Image.ANTIALIAS)
                image.save(instance.image.path, quality=100)

        # Grab the image extension
        _name, extension = os.path.splitext(instance.image.name)

        # Use old_name to create the desired new_name while keeping the same dirs
        old_name = instance.image.name.split('/')[-1] # e.g. IMG_3402.jpg
        new_name = instance.image.name.replace(old_name, f"{instance.name}_{instance.id}{extension}")
        
        # Rename if name is not the right one already
        if old_name != new_name:
            
            old_path = instance.image.path
            new_path = old_path.replace(old_name, f"{instance.name}_{instance.id}{extension}")
            
            instance.image.name = new_name # Assign the new name
            os.replace(old_path, new_path) # Replace with the new file
            instance.save(update_fields=['image']) # Save the instance

post_save.connect(resize_rename, sender=Item)
Amberclust
  • 23
  • 5
  • I believe there are a couple of ways to get around this issue. As far as I know the object's id is set **after** the object is saved. Checking out [this question](https://stackoverflow.com/q/14234917/11604566) will help you. – Fateme Fouladkar Dec 09 '22 at 17:58
  • If the intention is to have it in an unique form, you can make use of Python [uuid](https://docs.python.org/3/library/uuid.html) and replace the id for the uuid. Otherwise, you would have to rename every image after saving it. – Niko Dec 11 '22 at 04:24

1 Answers1

0

It is not possible because the id is assigned after saving. You have to use a post_save signal and then change the filename To do this add signal.py under app In the apps.py file, override the ready function to add the signals.py import statement

apps.py

from django.apps import AppConfig


class AppNameConfig(AppConfig):
    name = 'app_name'

    def ready(self):
        import app_name.signals

On init.py set default app config

init.py

default_app_config = 'app_name.apps.AppNameConfig'

signals.py

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

from .models import Item

@receiver(post_save, sender=Item)
def post_save(sender, instance: FilettoPasso, **kwargs):
    if kwargs['created']:        
        print(instance.id)
        instance.image.path = new path
        # rename file in os path
        ...
        instance.save()