47

Upon saving me model 'Products' I would like the uploaded image to be named the same as the pk for example 22.png or 34.gif I don't want to change the format of the image just the name. How can this be done? example of my model so far below...

image = models.ImageField(
        upload_to="profiles",
        height_field="image_height",
        width_field="image_width",
        null=True,
        blank=True,
        editable=True,
        help_text="Profile Picture",
        verbose_name="Profile Picture"
    )
    image_height = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
    image_width = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
jason
  • 2,895
  • 8
  • 26
  • 38

7 Answers7

104

You can pass a function into upload_to field:

def f(instance, filename):
    ext = filename.split('.')[-1]
    if instance.pk:
        return '{}.{}'.format(instance.pk, ext)
    else:
        pass
        # do something if pk is not there yet

My suggestions would be to return a random filename instead of {pk}.{ext}. As a bonus, it will be more secure.

What happens is that Django will call this function to determine where the file should be uploaded to. That means that your function is responsible for returning the whole path of the file including the filename. Below is modified function where you can specify where to upload to and how to use it:

import os
from uuid import uuid4

def path_and_rename(path):
    def wrapper(instance, filename):
        ext = filename.split('.')[-1]
        # get filename
        if instance.pk:
            filename = '{}.{}'.format(instance.pk, ext)
        else:
            # set filename as random string
            filename = '{}.{}'.format(uuid4().hex, ext)
        # return the whole path to the file
        return os.path.join(path, filename)
    return wrapper

FileField(upload_to=path_and_rename('upload/here/'), ...)
miki725
  • 27,207
  • 17
  • 105
  • 121
  • 1
    beat me to the function :( – Aidan Ewen Feb 28 '13 at 16:57
  • Sorry. I actually did not see your answer. Also when I posted mine noticed that there was another answer... – miki725 Feb 28 '13 at 16:59
  • the instance being passed would that be the model? – jason Feb 28 '13 at 17:01
  • 4
    no, no don't apologise. I hadn't posted the function, and that's what OP needed. Your answer was better and posted quicker than mine. +1 from me anyway. – Aidan Ewen Feb 28 '13 at 17:01
  • in my case it won't allow me to pass the instance like this... upload_to=path_and_rename(Profile,"profiles"), – jason Feb 28 '13 at 17:03
  • 3
    you are passing the called function. instead you should pass the function itself - `upload_to=path_and_rename, ...` – miki725 Feb 28 '13 at 17:06
  • @miki725 but its asking for two arguments, how will it know the path? I can see the function changes the name tho. just trying to understand :) – jason Feb 28 '13 at 17:08
  • this seems to work, think I need to understand it better tho, maybe a different question. thank you both :) – jason Feb 28 '13 at 17:15
  • 6
    The solution is working but when I am making migrations I am getting a value error saying 'Could not find wrapper' – Rohan Jun 02 '17 at 06:46
  • It would be better to use `splitext` from `os.path` to get original file extension – Ales Dakshanin Sep 28 '18 at 12:31
  • 4
    I'm getting `Could not find function wrapper` when implimenting that new function. What am I doing wrong? – Redgren Grumbholdt Apr 19 '19 at 13:10
  • 4
    For anyone that still runs into the issue `Could not find function wrapper`: https://stackoverflow.com/a/25768034/9493938 – Baily Mar 13 '21 at 17:08
32

Django 1.7 and newer won't make migration with function like this. Based on answer by @miki725 and this ticket, you need to make your function like this:

import os
from uuid import uuid4
from django.utils.deconstruct import deconstructible

@deconstructible
class UploadToPathAndRename(object):

    def __init__(self, path):
        self.sub_path = path

    def __call__(self, instance, filename):
        ext = filename.split('.')[-1]
        # get filename
        if instance.pk:
            filename = '{}.{}'.format(instance.pk, ext)
        else:
            # set filename as random string
            filename = '{}.{}'.format(uuid4().hex, ext)
        # return the whole path to the file
        return os.path.join(self.sub_path, filename)

FileField(upload_to=UploadToPathAndRename(os.path.join(MEDIA_ROOT, 'upload', 'here'), ...)
bmiljevic
  • 742
  • 8
  • 16
  • 1
    This also works with python 2.7 (Tested Django 1.9) whereas the accepted answer only works with >= 3.0 raising the error `Please note that due to Python 2 limitations, you cannot serialize unbound method functions (e.g. a method declared and used in the same class body). Please move the function into the main module body to use migrations. For more information, see https://docs.djangoproject.com/en/1.9/topics/migrations/#serializing-values` – dotcomly May 26 '16 at 00:28
  • 1
    No need for MEDIA_ROOT in UploadToPathAndRename(os.path.join(MEDIA_ROOT... FileField adds that later. – dmitri Sep 14 '20 at 08:07
  • How is `instance` and `filename` passed to the `__call__` method? Wouldn't it be necessary to write `upload_to=UploadToPathAndRename(...)(obj_instance, 'some_file_name')` ? Plz help me to understand this python magic haha – Eduardo Gomes Mar 16 '22 at 20:01
7

You can replace the string your assigning to upload_to with a callable as described in the docs. However, I suspect the primary key may not be available at the point the upload_to parameter is used.

Aidan Ewen
  • 13,049
  • 8
  • 63
  • 88
5

By default Django keeps the original name of the uploaded file but more than likely you will want to rename it to something else (like the object's id). Luckily, with ImageField or FileField of Django forms, you can assign a callable function to the upload_to parameter to do the renaming. For example:

from django.db import models
from django.utils import timezone
import os
from uuid import uuid4

def path_and_rename(instance, filename):
    upload_to = 'photos'
    ext = filename.split('.')[-1]
    # get filename
    if instance.pk:
        filename = '{}.{}'.format(instance.pk, ext)
    else:
        # set filename as random string
        filename = '{}.{}'.format(uuid4().hex, ext)
    # return the whole path to the file
    return os.path.join(upload_to, filename)

and in models field:

class CardInfo(models.Model):
    ...
    photo = models.ImageField(upload_to=path_and_rename, max_length=255, null=True, blank=True)

In this example, every image that is uploaded will be rename to the CardInfo object's primary key which is id_number.

M-Jamiri
  • 127
  • 2
  • 9
  • could you help understand how `instance` and `filename` values are passed with this assignment `upload_to=path_and_rename` ? I didn't catch this python magic. Thanks. – Eduardo Gomes Mar 16 '22 at 20:05
1

Another option, as following this answer https://stackoverflow.com/a/15141228/3445802, we found the issue when we need return path with %Y/%m/%d, example:

FileField(upload_to=path_and_rename('upload/here/%Y/%m/%d'), ...)

so, we handle it with this:

FileField(upload_to=path_and_rename('upload/here/{}'.format(time.strftime("%Y/%m/%d"))), ...)

Makesure the module time has been imported.

Community
  • 1
  • 1
agaust
  • 97
  • 3
  • 19
0

I do have a more customizable implementation of Aidan Ewen's solution.

What's new?

  • You can send the fields you want to use in the filenames as a list (as pre-ordered)
  • ^ one of them must be unique
  • ^ else, this list must include (at least one of) the tuple of unique together fields
  • ^ else, fields you sent are will be ignored and will use the uuid4 as filename

example 1:

image = models.ImageField(upload_to=PathAndRename('images/').wrapper)

filename = {pk}.{ext}
# default is pk for filenames

example 2:

name = models.CharField(max_length=20)  # not unique
image = models.ImageField(upload_to=PathAndRename('images/', ['name']).wrapper)

filename = {uuid4}.{ext}
# if given fields are did not accepted will use the uuid4

example 3:

name = models.CharField(max_length=20, unique=True)
no = models.CharField(max_length=10)
image = models.ImageField(upload_to=PathAndRename('images/', ['name','no']).wrapper)

filename = {name}_{no}.{ext}
# one unique field is enough to use all of the given fields in the filename

example 4:

name = models.CharField(max_length=20)  # not unique
no = models.CharField(max_length=10)  # not unique
image = models.ImageField(upload_to=PathAndRename('images/', ['name','no']).wrapper)

class Meta:
    unique_together = ('name', 'no')
    # (('name', 'no'),) is acceptable too or multiple unique togethers

filename = {name}_{no}.{ext}
# if one of the unique together fields exists in the given fields, will use all of the given fields in the filename

I may be forgot to give some more examples but you can understand from the code below:

class PathAndRename:
    """
    fields to use for naming, order is important
    """

    def __init__(self, path, fields_to_use=('pk',)):
        self.path = path
        self.fields_to_use = fields_to_use

    def wrapper(self, instance, filename):
        # multiple extensions
        ext = '.'.join(filename.split('.')[1:])

        # check the uniqueness of the fields given for filename
        if self.is_any_unique_exist(instance):
            # if any unique field exist in the given list
            # create filename by using given field values
            filename = '{}.{}'.format(self.get_filename_by_fields(instance), ext)
        # else check the existence of at least one unique together
        elif self.is_any_unique_together_exist(instance):
            # create filename by using given field values
            filename = '{}.{}'.format(self.get_filename_by_fields(instance), ext)
        # if any unique or unique together not exists
        else:
            # then create a filename by using uuid4
            filename = '{}.{}'.format(uuid4().hex, ext)

        # return the whole path to the file
        return os.path.join(self.path, filename)

    def is_any_unique_exist(self, instance):
        if 'pk' in self.fields_to_use:
            return True
        return any([instance._meta.get_field(field).unique for field in self.fields_to_use if hasattr(instance, field)])

    def is_any_unique_together_exist(self, instance):
        if hasattr(instance._meta, 'unique_together'):
            if isinstance(instance._meta.unique_together, (list, tuple)):
                for uniques in instance._meta.unique_together:
                    # if any one of the unique together set is exists in the fields to use
                    if all(map(lambda field: field in self.fields_to_use, uniques)):
                        return True
            else:
                if all(map(lambda field: field in self.fields_to_use, instance._meta.unique_together)):
                    return True
        return False

    def get_filename_by_fields(self, instance):
        return '_'.join([str(getattr(instance, field)) for field in self.fields_to_use])

WARNING: Every method-based solution to this upload_to problem is problematic for preformed migration files when you abandon using these solutions. If you use these solutions for a while and then delete them, the old migrations will fail because of the unexistence of these methods. (of course, you can fix this problem by modifying the old migration files)

aysum
  • 41
  • 1
  • 5
0

I changed the accepted answer a little bit for easy understanding.

def wrapper(instance, filename):
    ext = filename.split('.')[-1]
    # get filename
    if instance.pk:
        filename = '{}.{}'.format(instance.pk, ext) # do instance.username 
                                                    # if you want to save as username
    else:
        # set filename as random string
        filename = '{}.{}'.format(uuid4().hex, ext)
    # return the whole path to the file
    return os.path.join('path/to/save/', filename)

image = models.ImageField(upload_to=wrapper, default="/user.png", null=True, blank=True)
Aditya Rajgor
  • 953
  • 8
  • 14