[PYTHON] Django's ImageField

Preparation

PIL (Pillow) is required to handle ImageField, so if you are not in the Python runtime environment, install it below:

pip install pillow
pip3 install pillow # Python 3.x

Also, I need to define MEDIA_ROOT in settings.py, so add it:

settings.py


MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Also add the following to ʻurls.py`:

urls.py


urlpatterns = [
    url(r'^media/(?P<path>.*)$','django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
]

What is ImageField?

Various Fields can be defined in Django's Model, but there is ** ImageField ** as a Field for easily handling image files. Here is the simplest example:

class Image(models.Model):
    image = models.ImageField(upload_to='images/')

Now, when saving an image file, it will be saved with MEDIA_ROOT +'images / (original image file name). If the "original image file name" is covered, ** Django will arbitrarily add a strange suffix and it will not be overwritten. ** **

I don't want to use the original file name

First of all, I wanted to save the image using the primary key when saving it. For example, the original file name is car.png, but if the primary key for saving is ʻid = 123 (pk = 123) `, it will be renamed to 123.png.

ImageField can freely change not only the image save destination directory but also the file name by giving a function to the ʻupload_to` argument, but even if you do the following, for example, the id has not been taken yet when the function is called. So it doesn't work:

def get_image_path(self, filename):
    prefix = 'images/'
    name = self.id  #In this case, the ID has not been decided yet when creating a new one, so it will be None.!!No good
    extension = os.path.splitext(filename)[-1]
    return prefix + name + extension

Then, when self.id is None, it is one way to take max (id) + 1 from DB and use it, but it depends on DB, but the next id is not necessarily max (id). It is not strict because it does not become id) + 1.

According to StackOverFlow, it says, "Save it with an appropriate name for the time being, and if the ID is decided after saving, you should rename it to the correct file name" and tried that, but the actual file is certainly That's fine, but the path in the DB is still old, so it's strange when I see it with Django admin.

Later, for example, if you save it as 123.png and update the image, you cannot ** overwrite 123.png, and ** 123 will create an incomprehensible file called .png. That's because Django doesn't allow overwriting. To deal with this, you need to delete the old file in advance and then save it ...

I gave up saving the image file name with the primary key because it became troublesome when I thought about it.

You should keep it as UUID

If you want to create a normally unique filename, you can use the UUID, so I tried:

def get_image_path(self, filename):
    """Get a customized image path.

    :param self:instance(models.Model)
    :param filename:Original file name
    :return:Image path containing customized filenames
    """
    prefix = 'images/'
    name = str(uuid.uuid4()).replace('-', '')
    extension = os.path.splitext(filename)[-1]
    return prefix + name + extension

Now it is registered with a name such as images / 7f9a9970cc8645a99a2191c114856426.jpg. However, even if ImageField is updated or deleted on the management site as it is, there is a problem that the DB record disappears but the original file remains. I want to do something about this.

Delete old image files when updating or deleting

As the title suggests, I want to delete old files and make them cleaner when save () or delete (). It is better to save the old file path before updating / deleting and delete the old file after the actual processing is performed. This is where the decorator comes in.

def delete_previous_file(function):
    """Decorator implementation to delete old files that are no longer needed.

    :param function:Main function
    :return: wrapper
    """
    def wrapper(*args, **kwargs):
        """Wrapper function.

        :param args:Arbitrary argument
        :param kwargs:Arbitrary keyword argument
        :return:Main function execution result
        """
        self = args[0]

        #Get the file name before saving
        result = Image.objects.filter(pk=self.pk)
        previous = result[0] if len(result) else None
        super(Image, self).save()

        #Function execution
        result = function(*args, **kwargs)

        #Delete any files before saving
        if previous:
            os.remove(MEDIA_ROOT + '/' + previous.image.name)
        return result
    return wrapper

Prepare a function for such a decorator,

class Image(models.Model):
    @delete_previous_file
    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        super(Image, self).save()

    @delete_previous_file
    def delete(self, using=None, keep_parents=False):
        super(Image, self).delete()

    image = models.ImageField('image', upload_to=get_image_path)

By doing so, old files will be deleted when updating / deleting.

Recommended Posts

Django's ImageField
Use Django's ImageField on App Engine / Python
Django's order_by notes
Django's recommended library
Django's basic memorandum
About Django's ProxyModel