Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Access primary key in models.filefield(upload_to) location

I'd like to save my files using the primary key of the entry.

Here is my code:

def get_nzb_filename(instance, filename):
    if not instance.pk:
        instance.save() # Does not work.
    name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
    name_slug = re.sub('[-]+', '-', name_slug)
    return u'files/%s_%s.nzb' % (instance.pk, name_slug)

class File(models.Model):
    nzb = models.FileField(upload_to=get_nzb_filename)
    name = models.CharField(max_length=256)

I know the first time an object is saved the primary key isn't available, so I'm willing to take the extra hit to save the object just to get the primary key, and then continue on.

The above code doesn't work. It throws the following error:

maximum recursion depth exceeded while calling a Python object

I'm assuming this is an infinite loop. Calling the save method would call the get_nzb_filename method, which would again call the save method, and so on.

I'm using the latest version of the Django trunk.

How can I get the primary key so I can use it to save my uploaded files?


Update @muhuk:

I like your solution. Can you help me implement it? I've updated my code to the following and the error is 'File' object has no attribute 'create'. Perhaps I'm using what you've written out of context?

def create_with_pk(self):
    instance = self.create()
    instance.save()
    return instance

def get_nzb_filename(instance, filename):
    if not instance.pk:
        create_with_pk(instance)
    name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
    name_slug = re.sub('[-]+', '-', name_slug)
    return u'files/%s_%s.nzb' % (instance.pk, name_slug)

class File(models.Model):
    nzb = models.FileField(upload_to=get_nzb_filename, blank=True, null=True)
    name = models.CharField(max_length=256)

Instead of enforcing the required field in my model I'll do it in my Form class. No problem.

like image 426
Ty. Avatar asked Mar 16 '09 20:03

Ty.


4 Answers

You can create pre_save and post_save signals. Actual file saving will be in post_save, when pk is already created. Do not forget to include signals in app.py so they work. Here is an example:

_UNSAVED_FILE_FIELD = 'unsaved_file'


@receiver(pre_save, sender=File)
def skip_saving_file_field(sender, instance: File, **kwargs):
    if not instance.pk and not hasattr(instance, _UNSAVED_FILE_FIELD):
        setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
        instance.nzb = None


@receiver(post_save, sender=File)
def save_file_field(sender, instance: Icon, created, **kwargs):
    if created and hasattr(instance, _UNSAVED_FILE_FIELD):
        instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
        instance.save()
like image 195
Max Koniev Avatar answered Nov 10 '22 12:11

Max Koniev


It seems you'll need to pre-generate your File models with empty file fields first. Then pick up one and save it with the given file object.

You can have a custom manager method like this;

def create_with_pk(self):
    instance = self.create()
    instance.save()     # probably this line is unneeded
    return instance

But this will be troublesome if either of your fields is required. Because you are initially creating a null object, you can't enforce required fields on the model level.

EDIT

create_with_pk is supposed to be a custom manager method, in your code it is just a regular method. Hence self is meaningless. It is all properly documented with examples.

like image 32
muhuk Avatar answered Nov 10 '22 13:11

muhuk


You can do this by setting upload_to to a temporary location and by creating a custom save method.

The save method should call super first, to generate the primary key (this will save the file to the temporary location). Then you can rename the file using the primary key and move it to it's proper location. Call super one more time to save the changes and you are good to go! This worked well for me when I came across this exact issue.

For example:

class File( models.Model ):
    nzb = models.FileField( upload_to='temp' )

    def save( self, *args, **kwargs ):
        # Call save first, to create a primary key
        super( File, self ).save( *args, **kwargs )

        nzb = self.nzb
        if nzb:
            # Create new filename, using primary key and file extension
            oldfile = self.nzb.name
            dot = oldfile.rfind( '.' )
            newfile = str( self.pk ) + oldfile[dot:]

            # Create new file and remove old one
            if newfile != oldfile:
                self.nzb.storage.delete( newfile )
                self.nzb.storage.save( newfile, nzb )
                self.nzb.name = newfile 
                self.nzb.close()
                self.nzb.storage.delete( oldfile )

        # Save again to keep changes
        super( File, self ).save( *args, **kwargs )
like image 4
Giles Avatar answered Nov 10 '22 11:11

Giles


Context

Had the same issue. Solved it attributing an id to the current object by saving the object first.

Method

  1. create a custom upload_to function
  2. detect if object has pk
  3. if not, save instance first, retrieve the pk and assign it to the object
  4. generate your path with that

Sample working code :

class Image(models.Model):
    def upload_path(self, filename):
        if not self.pk:
            i = Image.objects.create()
            self.id = self.pk = i.id
        return "my/path/%s" % str(self.id)
    file = models.ImageField(upload_to=upload_path)
like image 3
vinyll Avatar answered Nov 10 '22 11:11

vinyll