Search code examples
pythondjangomodels

upload_to dynamically generated url to callable


I've seen a lot of post about this problem without really understanding how to solve it.

I have this model:

class Project(TimeStampedModel):
    name = models.TextField(max_length=100, default='no name')
    logo = models.ImageField()

I'd like to have my image saved to media root following this template:

<name>/logo/<filename>

At first glance, I would like to do:

logo = models.ImageField(upload_to="{}/logo/".format(name))

But it raises this error: AttributeError: 'TextField' object has no attribute 'model'

Using a callable would be fine, partially though:

def upload_to_project(self, filename):
    url = ("%s/%s") % (self.name, filename)
    return url

and using:

logo = models.ImageField(upload_to=upload_to_project)

at least I have: <name>/<filename>

But how to pass the argument in this case? I'd like to reuse my function to upload in other subfolders, not only logo as:

<name>/logo/<filename>
<name>/history/<filename>
<name>/whatever/<filename>

Any idea on what I could do?


Solution

  • It looks like (re-reading your post it's not 100% clear) what you want is a partial application. Good news, it's part of Python's stdlib:

    import os
    from functools import partial
    
    def generic_upload_to(instance, filename, folder):
        return os.path.join(instance.name, folder, filename)
    
    
    class Project(TimeStampedModel):
        name = models.TextField(max_length=100, default='no name')
        logo = models.ImageField(
            upload_to=partial(generic_upload_to, folder="logo")
            )
    

    Note that this implementation assumes instance has a name attribute... if the instance attribute you want to use as first part has to be configurable too you can rewrite your upload_to as:

    def generic_upload_to(instance, filename, folder, attrname):
        return os.path.join(getattr(instance, attrname), folder, filename)
    

    then use it as

    class Project(TimeStampedModel):
        name = models.TextField(max_length=100, default='no name')
        logo = models.ImageField(
            upload_to=partial(generic_upload_to, attrname="name", folder="logo")
            )
    

    And if you have more than one FileField or ImageField in your model and don't want to repeat the attrname part:

    class Something(TimeStampedModel):
        my_upload_to = partial(generic_upload_to, attrname="label")
    
        label = models.CharField(max_length=100, default='no label')
        logo = models.ImageField(
            upload_to=partial(my_upload_to, folder="logo")
            )
        attachment = models.FileField(
            upload_to=partial(my_upload_to, folder="attachment")
            )