0

I have a Django model for a Book which has a slug field that's a hash based on its pk. It also has a thumbnail which is saved to a path including that slug.

In Admin, if I create and save the Book without a thumbnail, and then add the thumbnail and save the Book again, this works: the thumbnail is saved to /media/books/<slug>/foo.jpg.

BUT if I create the Book with a thumbnail image, and save it, the thumbnail is saved before the slug can be generated, so it's saved to /media/books/foo.jpg. This because files are saved before the model.

I'd like to always include slug in the thumbnail's path, but I can't work out how to delay saving the thumbnail until after the slug has been generated. Any ideas?

from django.db import models
from hashes import Hashids


def upload_path(instance, filename):
    return "/".join([books, instance.slug, filename])


class Book(models.Model):
    title = models.CharField(null=False, blank=False, max_length=255)
    slug = models.SlugField(max_length=10, null=False, blank=True)

    thumbnail = models.ImageField(
        upload_to=upload_path, null=False, blank=True, default=""
    )

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        if not self.slug:
            # Now we have a pk, generate a slug if it doesn't have one.
            hashids = Hashids(salt="my salt", min_length=5)
            self.slug = hashids.encode(self.pk)
            kwargs["force_insert"] = False
            self.save(*args, **kwargs)

(I know that Hashids aren't secure; that someone could figure out the pk from the slug. I'm fine with that for this use case.)

Phil Gyford
  • 13,432
  • 14
  • 81
  • 143

1 Answers1

0

I think the answer is to move the file to the correct location after generating the pk. the model:

import os
from django.conf import settings
from django.db import models
from hashes import Hashids

def upload_path(instance, filename):
    return "/".join([books, instance.slug, filename])

class Book(models.Model):
    # field definitions here

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        if not self.slug:
            # Now we have a pk, generate a slug if it doesn't have one.
            hashids = Hashids(salt="my salt", min_length=5)
            self.slug = hashids.encode(self.pk)

        if self.thumbnail and f"/{self.slug}/" not in self.thumbnail.path:
            # Move the thumbnail to correct location.
            initial_path = self.thumbnail.path
            filename = os.path.basename(initial_path)
            new_name = upload_path(self, filename)
            new_path = os.path.join(settings.MEDIA_ROOT, new_name)

            if not os.path.exists(os.path.dirname(new_path)):
                # Make the slug directory if it doesn't exist.
                os.makedirs(os.path.dirname(new_path))

            os.rename(initial_path, new_path)

            self.thumbnail.name = new_name
            kwargs["force_insert"] = False
            super().save(*args, **kwargs)
Phil Gyford
  • 13,432
  • 14
  • 81
  • 143