Django管理画面で選択できる外部キーを絞る(Filterする)方法

この記事は[Django Advent Calendar 2019](https://qiita.com/advent-calendar/2019/django) 3日目の記事です。
（アドベントカレンダーは）初投稿です。

# はじめに

ここ１ヶ月くらいDjangoで個人的にアプリを作ってる非エンジニアの人です。
そのアプリで外部キーを使用したところ、プルダウンを開くと100件以上表示されてしまうので、
なんとか絞れないかなと思いました。

先に結論を言えば、`limit_choices_to`を使うことで、外部キーで表示されるレコードをfilterすることが可能です。
どのように使えば良いか、実際にアプリを作成して試してみます。

# この記事の為に作ったアプリ

説明の為に簡単なアプリを作成しました。Modelだけです。
ちなみにテーマとしては、2019年にFA権を取得した選手達がどの球団に行くかの予想を登録するアプリです。
もうだいたい決まったから実用性はないな！
https://github.com/shimayu22/fa_expects_app/

FaExpectsテーブルでは、Playersテーブルを外部キー設定し、登録されている選手達がプルダウンで選択できるようにしています。
選手データも用意しているので読み込むだけでお試しできます。
使い方は[README](https://github.com/shimayu22/fa_expects_app/)を参照してください。

# こういうこと
https://github.com/shimayu22/fa_expects_app/tree/part1

![スクリーンショット 2019-11-23 23.01.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/ea5ede6b-2f4f-3eaf-9955-35d0e3b108ac.png)

※`python manage.py runserver`で検証しています
http://127.0.0.1:8000/admin/fa_expects/faexpects/add/
の「選手」のプルダウンを開くと、
画像のようにズラーっと表示されてしまいます。

管理画面でFilterできんのか、という話です。

# limit_choices_toを利用する
[DjangoドキュメントのForeignKey.limit_choices_to](https://docs.djangoproject.com/ja/2.2/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to)より、
> このフィールドがModelFormまたはadmin を使用してレンダリングされるときに、このフィールドで使用可能な選択肢に制限を設定します（デフォルトでは、クエリセット内のすべてのオブジェクトを選択できます）。
辞書、Qオブジェクト、または辞書またはQオブジェクトを返す呼び出し可能オブジェクトのいずれかを使用できます。
(Google翻訳)

ということなので、`limit_choices_to`を使えば選択できる項目をFilterすることができます。

## 実際にやってみた
https://github.com/shimayu22/fa_expects_app/tree/part2

FaExpectsのplayer_idにlimit_choices_toを追加しました。

```models.py
    player_id = models.ForeignKey(
        Players,
        on_delete=models.CASCADE,
        verbose_name="選手",
        limit_choices_to={"position":1,}
    )
```

今回はpositionが「1」（投手）だけ選択できるようにします。
これで、
http://127.0.0.1:8000/admin/fa_expects/faexpects/add/
の「選手」のプルダウンを開くと、

![スクリーンショット 2019-11-24 22.52.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/0da47ea5-c8c4-a029-dac2-32e4f3100b70.png)

見事、投手として登録した選手だけ表示されました！

## 検索キーワードを使用する

上記の例だと、`position == 1`など、イコールでしか絞れませんが、検索キーワードを使用することで、以上、以下などを用いて絞ることが可能です。

```models.py
    player_id = models.ForeignKey(
        Players,
        on_delete=models.CASCADE,
        verbose_name="選手",
        limit_choices_to={"position": 1,
                          "age__lt": 33}
    )
```

上記は`position == 1 and age < 33`（投手かつ３３歳未満）という条件を付けます。
項目名`age`に`__lt`（アンダーバー２つ）をつけてると「～未満」という条件が加わります。

![スクリーンショット 2019-11-24 23.01.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/1a201cb0-00e5-b508-eef2-e084c4301556.png)

選択できる選手が少なくなりましたね！
その他のキーワードについては下記の記事がとてもよくまとまっています。

参考:[Django データベース操作 についてのまとめ#検索キーワードの一覧](https://qiita.com/okoppe8/items/66a8747cf179a538355b#%E6%A4%9C%E7%B4%A2%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E3%81%AE%E4%B8%80%E8%A6%A7)

## Qオブジェクトを利用する
これまでは辞書型で検索条件を指定していましたが、Qオブジェクトでも指定することが可能です。

```models.py
from django.db.models import Q

~~~省略~~~
    player_id = models.ForeignKey(
        Players,
        on_delete=models.CASCADE,
        verbose_name="選手",
        limit_choices_to=Q(position=4) | Q(position=7),
    )
```

Qオブジェクトだと、このようにOR条件を指定できます。
上記では「ポジションが二塁手 or 外野手」という意味になります。

![スクリーンショット 2019-11-24 23.37.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/8dee8b7a-e0ea-7576-a685-5c3ac71761ee.png)

見事、二塁手または外野手だけ表示することができました！

## limit_choices_to用の辞書を作る
https://github.com/shimayu22/fa_expects_app/tree/part3

べた書きだと柔軟性がないので、関数で辞書型を作って渡してあげます。
今回はサクッと作るために、条件を指定する用のテーブルを作りました。
`Models.py`に`RequestedConditions`クラスを追加しています。
（長いのでModels.pyで確認してください）

そして、`RequestedConditions`テーブルの最新レコードからlimit_choices_to用の辞書を作成するための関数を作成しました。

```Models.py
〜〜〜省略〜〜〜

def set_players_condition():
    condition =  RequestedConditions.objects.latest('pk')
    condition_dict = {}
    if condition.age > 0:
        condition_dict["age__lt"] = condition.age
    
    if condition.position > 0:
        condition_dict["position"] = condition.position
    
    if condition.dominant_hand > 0:
        condition_dict["dominant_hand"] = condition.dominant_hand

    return condition_dict

〜〜〜省略〜〜〜
```

まず、
http://127.0.0.1:8000/admin/fa_expects/requestedconditions/add/
より、「年齢」「ポジション」「利き手」を設定し、保存します。
（全部設定しなければ全部表示されます）

<img width="599" alt="スクリーンショット 2019-11-26 23.31.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/884f6a78-0bcd-ea66-1850-311facf69bff.png">


登録後、
http://127.0.0.1:8000/admin/fa_expects/faexpects/add/
の選手を見てみると、条件にあった選手のみ表示されます。

<img width="342" alt="スクリーンショット 2019-11-26 23.31.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/4e9d9c4a-d7e1-1f58-6de7-c0978a47e961.png">

やったぜ。

# おわりに
モデルだけでちょろっとやりたい時はこんな感じでできます。
Qオブジェクトも組み合わせればもっと柔軟にできそうですね。
色々いじって試してみてください。

# おまけ
冷静に考えれば当たり前かもしれませんが、
`limit_choices_to={"age__lt":RequestedConditions.objects.latest('pk').age}`
みたいな感じで書いてしまうと、最初にmigrateした時に「そんなテーブルないぞ」というエラーが出ます。
そら（まだ作成されていないテーブルを参照しようとしたら）そう（エラーが出る）よ（当たり前じゃないか）。

# あまり関係ない補足

1. FA宣言した選手達ではなく、FA権を取得した選手達です（間違いがあったらゴメンね）
2. 鈴木大地（ロ）は色々守れすぎなので、2019年一番多く守ってるっぽい（一）としました
3. 私はちなヤクちなハムです
4. 来シーズンもやきう楽しみですね！


# 参考

[Django データベース操作 についてのまとめ](https://qiita.com/okoppe8/items/66a8747cf179a538355b)

また、Django全般で下記記事が参考になりました。ありがとうございます！
[[Python] Djangoチュートリアル - 汎用業務Webアプリを最速で作る](https://qiita.com/okoppe8/items/54eb105c9c94c0960f14)
[[Django] モデルフィールド 設定テンプレート](https://qiita.com/okoppe8/items/a1149b2be54441951de1)