Django FileField with upload_to determined at runtime
Asked Answered
C

6

150

I'm trying to set up my uploads so that if user joe uploads a file it goes to MEDIA_ROOT/joe as opposed to having everyone's files go to MEDIA_ROOT. The problem is I don't know how to define this in the model. Here is how it currently looks:

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to='.')

So what I want is instead of '.' as the upload_to, have it be the user's name.

I understand that as of Django 1.0 you can define your own function to handle the upload_to but that function has no idea of who the user will be either so I'm a bit lost.

Thanks for the help!

Consueloconsuetude answered 27/7, 2009 at 21:22 Comment(0)
P
281

You've probably read the documentation, so here's an easy example to make it make sense:

def content_file_name(instance, filename):
    return '/'.join(['content', instance.user.username, filename])

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=content_file_name)

As you can see, you don't even need to use the filename given - you could override that in your upload_to callable too if you liked.

Pellerin answered 27/7, 2009 at 21:56 Comment(13)
Yeah, it probably does belong in docs - it's a reasonably FAQ on IRCPellerin
Does this work with ModelForm? I can see that instance has all the attributes of the class model, but there are no values (just a str of the field name). In the template, user is hidden. I may have to submit a question, I have been googling this for hours.Eldreeda
Yes it works, and yes you should ask a new question (or ask for help on #django irc)Pellerin
Oddly enough this is failing on me in basically this same setup. instance.user has no attributes on it.Guess
In this callable function, can I access the Content class id, before it is saved? I would like to prefix or sufix the filename with the object id, so I can know to whom the file belongs.Simard
No, you can't. To quote the documentation, "In most cases, this object will not have been saved to the database yet, so if it uses the default AutoField, it might not yet have a value for its primary key field."Pellerin
You might want to use os.path.join instead of '/'.join to make sure it also works on not-Unix systems. They may be rare, but it's good practice ;)Proliferate
In fact, it's better to not use os.path.join. You're dealing with a Django storage, not the operating system directly and it expects '/' on both environments.Pellerin
So just to be clear here, the file still gets uploaded to the MEDIA_ROOT defined in settings.py?Jackstay
If you're using the default FileSystemStorage, then yes. The FileField only ever contains the relative name and path.Pellerin
Hi, I tried the same code, put them in models.py, but get error Content object has no attribute 'user'.Montes
The function cannot be static method in model; makemigrations rightfully complains.Camarena
Just a note - I tried changing "content" to something else but then my adblocker got invoked, so "content" works fine at least!Zermatt
J
13

This really helped. For a bit more brevity's sake, decided to use lambda in my case:

file = models.FileField(
    upload_to=lambda instance, filename: '/'.join(['mymodel', str(instance.pk), filename]),
)
Jewelljewelle answered 17/2, 2014 at 23:23 Comment(4)
This didn't work for me in Django 1.7 using migrations. Ended up creating a function instead and the migration took.Erastes
Even if you can't get lambda to work using the str(instance.pk) is a good idea if you have problems with files overwriting when you don't want them to.Cubage
instance does not have a pk before saving. It only works for updates not creations (inserts).Beebread
lambda doesn't work in migrations operations because it cant be serialized according to the docsTenantry
L
6

A note on using the 'instance' object's pk value. According to the documentation:

In most cases, this object will not have been saved to the database yet, so if it uses the default AutoField, it might not yet have a value for its primary key field.

Therefore the validity of using pk depends on how your particular model is defined.

Leukoderma answered 6/1, 2017 at 19:57 Comment(1)
I have getting None as the value. I can't figure out how to fix it. can you explain in a bit detail.Cockcrow
D
4

If you have problems with migrations you probably should be using @deconstructible decorator.

import datetime
import os
import unicodedata

from django.core.files.storage import default_storage
from django.utils.deconstruct import deconstructible
from django.utils.encoding import force_text, force_str


@deconstructible
class UploadToPath(object):
    def __init__(self, upload_to):
        self.upload_to = upload_to

    def __call__(self, instance, filename):
        return self.generate_filename(filename)

    def get_directory_name(self):
        return os.path.normpath(force_text(datetime.datetime.now().strftime(force_str(self.upload_to))))

    def get_filename(self, filename):
        filename = default_storage.get_valid_name(os.path.basename(filename))
        filename = force_text(filename)
        filename = unicodedata.normalize('NFKD', filename).encode('ascii', 'ignore').decode('ascii')
        return os.path.normpath(filename)

    def generate_filename(self, filename):
        return os.path.join(self.get_directory_name(), self.get_filename(filename))

Usage:

class MyModel(models.Model):
    file = models.FileField(upload_to=UploadToPath('files/%Y/%m/%d'), max_length=255)
Daberath answered 24/6, 2019 at 10:53 Comment(0)
I
1

If you have a user instance, let there be a quick setup to generate

<model-slug>/<username>-<first_name>-<last_name>/filename-random.png

eg: /medias/content/ft0004-john-doe/filename-lkl9237.png


def upload_directory_name(instance, filename):

    user = getattr(instance, 'user', None)
    if user:
        name = f"{user.username}-{user.get_full_name().replace(' ', '-')}"
    else:
        name=str(instance)
    model_name = instance._meta.verbose_name.replace(' ', '-')
    return str(os.path.pathsep).join([model_name, name, filename])


class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=upload_directory_name)


[A Modified Version of @SmileyChris ]

Insurgence answered 21/1, 2021 at 7:56 Comment(0)
I
1

I wanted to change the upload path in runtime, and none of the solutions were suitable for this need.

this is what I've done:

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=DynamicUploadPath.get_file_path)


class ContentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Content
        fields = '__all__'


class UploadDir(models.TextChoices):
    PRODUCT = 'PRD', _('Product')
    USER_PROFILE = 'UP', _('User Profile')


class DynamicUploadPath:
    dir: UploadDir = None

    @classmethod
    def get_file_path(cls, instance, filename):
        return str(cls.dir.name.lower() + '/' + filename)


def set_DynamicUploadPath(dir: UploadDir):
    DynamicUploadPath.dir = dir


class UploadFile(APIView):
    parser_classes = (MultiPartParser, FormParser)

    def post(self, request):
        # file save path: MEDIA_ROOT/product/filename
        set_DynamicUploadPath(UploadDir.PRODUCT)

        # file save path: MEDIA_ROOT/user_profile/filename
        # set_DynamicUploadPath(UploadDir.USER_PROFILE)

        serializer = ContentSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)
Italianate answered 13/2, 2022 at 6:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.