DjangoのAdminサイトのリスト表示で論理削除したレコードを表示しない方法

・・・がよくわかんなかったんだけどそれなりにできてしまって、このやり方に全く自信がないという話です。

    • -

地域と店舗があって多対1の関係とします。「地域 has many 店舗」です。
テーブルのカラムにdeleted_atというDateTime型のカラムを持たせてこれに値が入っていたら削除されたレコードだと見なします。
これを「論理削除」ということにします。
この場合、削除されてなかったらNULLになるので、削除されていないレコードをすべて取得したいときは以下のようなSQLを使います。

SELECT * FROM shops WHERE deleted_at IS NULL

一方で本当に削除する場合は「物理削除」ということにします。

    • -

さて。
DjangoのAdminサイトでは物理削除が行われるので、この挙動を変更したいです。
これは以下のようにモデルクラスでdelete()メソッドをオーバーライドすれば良さそうです。

class Shop(models.Model):
    def delete(self):
        self.deleted_at = datetime.datetime.now()
        self.save()

これで「削除」するとdeleted_atに現在時刻が入るようになります。
次に、レコードを取得するときにはデフォルトでdeleted_at IS NOT NULLなレコードを除外したいです。
http://michilu.com/django/doc-ja/model-api/を読むと、モデルにデータベースアクセスのインターフェースを提供しているのはマネージャと呼ばれるクラスであり、これは拡張可能なようです。

Manager.get_query_set() メソッドをオーバライドすれば、 Manager のベー スの QuerySet をオーバライドできます。 get_query_set() は必要なプロ パティを備えた QuerySet を返さねばなりません。
例えば、以下のモデルには 二つの マネジャがあります。片方は全てのオブジェ クトを返し、もう一方は Roald Dahl の書いた本だけを返します:

こんな風に書いてあるので、コレのまねをしてget_query_set()メソッドをオーバーライドしたいと思います。

class ShopManager(models.Manager):
    def get_query_set(self):
        return super(ShopManager, self).get_query_set().filter(
            deleted_at__isnull=True,
            area__deleted_at__isnull=True
        )

class Shop(models.Model):
    objects = ShopManager()

こんな感じにしました。
店舗とそれが属する地域が「論理削除」されていなければ可視なレコードです。
なんか、すごくいい感じです。
これでAdminサイトを見てみると・・・ふつーに表示されてる!!
論理削除されたはずのレコードがふつーにリスト表示されています。
なぜ??
クリックして編集用の画面を表示しようとすると404になるのでこの挙動は問題なさそうです。
でもリストには表示されてる。
うーん。
小1時間悩んだ末、しょうがないのでDjangoのソースを見てみることにしました。
たぶん該当箇所はdjango.admin.views.main.pyのChangeList()だと見当をつけました。change_list()の中で呼ばれてる関数です。

class ChangeList(object):
    def __init__(self, request, model):
        self.model = model
        self.opts = model._meta
        self.lookup_opts = self.opts
        self.manager = self.opts.admin.manager

        (中略)

        self.query_set = self.get_query_set()

get_query_set()はさっきオーバーライドしたばかりなので気になります。
で、self.managerはself.opts.admin.managerです。で、self.optsはmodel._metaです。
ここは野生の勘で、「きっとShopクラスの中のAdminクラスのmanagerをカスタムマネージャにすればいいんじゃないか?」と思いました。

class Shop(models.Model):
    class Admin:
        manager = ShopManager()

これでAdminサイトを見てみると・・・リストから消えてる!!
論理削除したレコードがリストから消えてます。まさに思い描いていた動作です。
でも、、これでいいのか?
全く自信がありません。。