Search code examples
pythondjangodjango-modelsdjango-file-upload

How to save a file to a model using upload_to for dynamic paths


I am trying to create a folder for each users to put their project in. So their file will have the path ..\project\id\filename, id is the user id and filename is the name of the file. Now using the arguments allowed for upload_to (instance and filename) in the Filefield, I realize that instance.id will be None and the path to the file will be ..\project\None\filename instead of ..\project\id\filename.

Now reading the Django documentation upload_to I saw this:

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.

My interpretation is that creating a new record and user_directory_path are not instantiated at the same time, that is, when I call create on Project model, instance.id will be None. My question is now, is there a way to get around this? While I see upload_to convenient, it is not necessarily convenient for dynamic path such as the one I am doing. I was thinking of creating the record, then adding the file path in an update, but I am in search of a way that can save everything in one step.

models.py
def user_directory_path(instance, filename):
    # file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
    return 'project/{0}/{1}'.format(instance.user.id, filename)


class Project(models.Model):

     email = models.ForeignKey(User,
                          to_field="email",
                          max_length=50
    )
    title = models.CharField(max_length=100)
    date_created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    file = models.FileField(upload_to=user_directory_path, validators=[validate_file_type], null=True)

This is views.py when the form passes validation. Notice user_directory_path is called just before the create.

email = request.user.email
title = request.POST.get('title', '')
file = request.FILES['file']
filename = file.name
instance = Usermie.objects.get(email=request.user.email)

# Save to model
user_directory_path(instance=instance, filename=filename)
Project.objects.create(
    title=title,  file=file,
)

Solution

  • If, as you say, the id that you want to use in the file path is the id of the User, not the id of the Project.. then there's no problem because the User already exists when you are saving the Project. Since email is a foreign key to User, you would just do:

    def user_directory_path(instance, filename):
        # file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
        return 'project/{0}/{1}'.format(instance.email.id, filename)
    

    But I will point out that, in the Django way of doing things, making a field called email that is a foreign key to User is actually pretty confusing. The field in the database will be called email_id.. and the value of the model field will return an instance of User.. not the actual email address, even though the email address is what's stored in the column. To get the email address you'd need to do one of:

    myproject.email.email    
    myproject.email_id
    

    Neither one is very clear. So unless you have a really good reason for doing it like that, you should call the field user and eliminate the to_field='email'. Allow Django to join the tables via id, which is the default behavior.

    Then if you need the user email address you can get it any time via

     myproject.user.email  
    

    And the bonus is that if the user changes their email address it will change everywhere, you don't have to rely on cascaded updates to fix all the foreign keys.

    Trust me, when using Django you want to do ForeignKey by id (the default) unless there's a reason...