Search code examples
djangofile-uploaddjango-views

dynamic file upload path with current instance id


I have a form which gets current logged in user, some inputs and a file:

class AddItemForm(ModelForm):
    class Meta:
        model = Item
        exclude = ['user']

For this form a have a view:

item_form = AddItemForm(request.POST, request.FILES)
if item_form.is_valid():
    item = item_form.save(commit=False)
    item.user = request.user
    item.save()

for this item's file field i am using upload_to feature. here is my modal:

class Item(models.Model):
    user = models.ForeignKey(User)
    cover_image = models.FileField(upload_to=get_upload_path)

def get_upload_path(instance, filename):

    return "items/user_{user_id}/item_{item_id}/{filename}".format(user_id=instance.user.id, item_id=instance.id,filename=filename)

problem is that i can't see the current instance id in uploaded path because of following line:

item = item_form.save(commit=False)

it has not instance id yet and instead of current item id it create user_1/item_NONE/file

how can i set id to this path?

thanks in advance


Solution

  • Here I've found idea && code based on using the post_save signal, when object created move from temp directory to specified one in model class:

    use_key and upload_to are optional. use_key defaults to False. If it is True then the id of the instance will be used as a prefix for the new file as there is the potential for overwriting now that we are moving the file. upload_to will simply define the temporary directory to upload the files to initially.

    from django.db.models import ImageField, FileField, signals
    from django.dispatch import dispatcher
    from django.conf import settings
    import shutil, os, glob, re
    from distutils.dir_util import mkpath
    
    class CustomImageField(ImageField):
        """Allows model instance to specify upload_to dynamically.
    
        Model class should have a method like:
    
            def get_upload_to(self, attname):
                return 'path/to/{0}'.format(self.id)
        """
        def __init__(self, *args, **kwargs):
            kwargs['upload_to'] = kwargs.get('upload_to', 'tmp')
    
            try:
                self.use_key = kwargs.pop('use_key')
            except KeyError:
                self.use_key = False
    
            super(CustomImageField, self).__init__(*args, **kwargs)
    
        def contribute_to_class(self, cls, name):
            """Hook up events so we can access the instance."""
            super(CustomImageField, self).contribute_to_class(cls, name)
            dispatcher.connect(self._move_image, signal=signals.post_save, sender=cls)
    
        def _move_image(self, instance=None):
            """
                Function to move the temporarily uploaded image to a more suitable directory 
                using the model's get_upload_to() method.
            """
            if hasattr(instance, 'get_upload_to'):
                src = getattr(instance, self.attname)
                if src:
                    m = re.match(r"%s/(.*)" % self.upload_to, src)
                    if m:
                        if self.use_key:
                            dst = "%s/%d_%s" % (instance.get_upload_to(self.attname), instance.id, m.groups()[0])
                        else:
                            dst = "%s/%s" % (instance.get_upload_to(self.attname), m.groups()[0])
                        basedir = "%s%s/" % (settings.MEDIA_ROOT, os.path.dirname(dst))
                        mkpath(basedir)
                        shutil.move("%s%s" % (settings.MEDIA_ROOT, src),"%s%s" % (settings.MEDIA_ROOT, dst))
                        setattr(instance, self.attname, dst)
                        instance.save()
    
        def db_type(self):
            """Required by Django for ORM."""
            return 'varchar(100)'
    
    
    class Image(models.Model):
        file = CustomImageField(use_key=True, upload_to='tmp')
    
        def get_upload_to(self, attname):
            return 'path/to/{0}'.format(self.id)