Djangoでファイルアップロードの際にファイル名にprimary keyを使う方法

作成: 2019年02月25日

更新: 2019年02月25日

やりたいこと

DjangoのmodelのFileFieldはupload_to=で指定することで保存する際のディレクトリ、ファイル名を指定できる。このときにディレクトリ名、ファイル名にmodelのprimary keyを使用したい。

問題点

Djangoのprimary keyは通常自動生成されるが、これはmodelの保存時に自動生成されるためmodel生成時に呼ばれるupload_toで用いることができない(無理やり呼ぶとNoneになる)。
max(id)+1のようにしてもいいが生成されるidがmax(id)+1である保証がない。

解決法

以下を参考にして作成した
python - Django admin file upload with current model id - Stack Overflow

from django.db import models
from django.db.models import CharField, FileField, DateTimeField, IntegerField
from django.utils import timezone
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
import os.path

def md_file_path(instance, filename):
    fn, ext = os.path.splitext(filename)
    return "article/{id}/{id}{ext}".format(id=instance.pk, ext=ext)

class Article(models.Model):
    title = CharField(max_length=128)
    article = models.FileField(upload_to=md_file_path)
    post_date = DateTimeField(default=timezone.now)


_UNSAVED_FILEFIELD = 'unsaved_filefield'

@receiver(pre_save, sender=Article)
def skip_saving_file(sender, instance, **kwargs):
    if not instance.pk and not hasattr(instance, _UNSAVED_FILEFIELD):
        setattr(instance, _UNSAVED_FILEFIELD, instance.article)
        instance.article = None

@receiver(post_save, sender=Article)
def save_file(sender, instance, created, **kwargs):
    if created and hasattr(instance, _UNSAVED_FILEFIELD):
        instance.article = getattr(instance, _UNSAVED_FILEFIELD)
        instance.save()
        instance.__dict__.pop(_UNSAVED_FILEFIELD)

上のコードはmdファイルをアップロードするためのコードなので必要に応じて適宜読み換えてほしい。

@receiver

というデコレータを用いてpre_save、post_saveシグナルと関数を紐づける。シグナルについては以下を参照。
Signals | Django documentation | Django
pre_saveはDjangoのsaveメソッドの最初に呼ばれて、post_saveはDjangoのsaveメソッドの最後に呼ばれる。sender=でモデルを指定する。
skip_saving_fileはinstanceに_UNSAVED_FILEFIELDという属性を付加して、その値をarticleにしている。そしてinstanceのarticle(FileField)を一時的にNoneにしている。
save_fileでは_UNSAVED_FILEFIELDに避難させていたarticleをinstance.articleに戻し、instance.save()を改めて実行している。_UNSAVED_FILEFIELDはもう不要なのでinstance.__dict__.pop(_UNSAVED_FILEFIELD)で属性ごと削除している。
まとめると_UNSAVED_FILEFIELDというフィールドにarticleを避難させた状態でsaveを行い、primary keyが生成された後で避難させたarticleを戻すという処理を行わせている。
これでmdファイルを保存するとmedia/article/1/1.md、media/article/2/2.md……のように保存される。