Django 複数選択フォームを動的に更新する(get_form_kwargsとMultipleChoiceField)

## 環境
Windows 11 Home
Python 3.10.2
Django 4.0.2
venv利用あり

## 背景
フォームに表示する選択肢をModelから取得したい。
Modelが更新されたらフォームの選択肢も更新されてほしいが、フォームにModelから読み込むコードを書いても、もう1度`manage.py runserver`されるまで更新されなかった。
views.pyを用いて動的にフォームの選択肢を更新する方法を記載する。

ゴールイメージはこんな形
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/209f9484-cfac-8078-e864-0c061fc5b7e8.png)

## 手順1
まず適当なMpdelを作成し、管理画面で更新できるようにする。

animalアプリケーションを作成
`python manage.py startapp animal`

## 手順2
各種pyファイルを更新する

```python:mysite\settings.py
INSTALLED_APPS = [
    ...
    'animal',
]
```

```python:mysite\urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('animal/', include('animal.urls')),
]
```

```python:animal\models.py
from django.db import models

class AnimalModel(models.Model):
    animal = models.CharField(verbose_name='動物', max_length=30, primary_key=True)
```

```python:animal\admin.py
from django.contrib import admin

from .models import AnimalModel

class AnimalModelClass(AnimalModel):
    class Meta:
        verbose_name = ('動物')
        verbose_name_plural = ('動物')

@admin.register(AnimalModelClass)
class AnimalModelAdmin(admin.ModelAdmin):
    list_display = ['animal']
    search_fields = ['animal']
```

```python:animal\urls.py
from django.urls import path
from . import views

app_name = 'animal'

urlpatterns = [
    path('', views.IndexView.as_view(), name='index')
]
```

```python:animal\views.py
from django.shortcuts import render
from django.views.generic.edit import FormView
from .models import AnimalModel
from .forms import AnimalForm

class IndexView(FormView):
    template_name = 'animal/index.html'
    form_class = AnimalForm

    def get_form_kwargs(self, *args, **kwargs):
        kwgs = super().get_form_kwargs(*args, **kwargs)

        products = AnimalModel.objects.values_list('animal', flat=True)
        choice_ls = [(product, product) for product in products]
        kwgs['categories'] = choice_ls
        return kwgs

    def form_valid(self, form):
        print("hello")
        message = f"選択された動物は{form.cleaned_data['animal']}で、備考は{form.cleaned_data['comment']}です。"
        context = {'form':form, 'msg':message}
        return render(self.request, self.template_name, context)
```

```python:animal\forms.py
from django import forms
from .models import AnimalModel

class AnimalForm(forms.Form):
    animal = forms.MultipleChoiceField()
    comment = forms.CharField(
        label='備考',
        max_length=200,
        required=False,
        widget=forms.TextInput()
    )

    class Meta:
        model = AnimalModel
        field = "__all__"

    def __init__(self, categories=None, *args, **kwargs):
        self.base_fields['animal'].choices = categories
        super().__init__(*args, **kwargs)
```

```python:animal\templates\animal\index.html
<body>
    <form method='POST'>
        {% csrf_token %}
        <table>
            {{ form.as_table}}
        </table>
        <button type='submit'>POST</button>
    </form>
    {{ msg }}
</body>
```
## 手順3
migrateしてRunserverする
`python manage.py makemigrations`
`python manage.py migrate`
`python manage.py runserver`

## 手順4
管理画面にログインしanimalモデルを更新する
http://127.0.0.1:8000/admin
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/0ef4b09f-ff87-13dc-9eae-47c929796068.png)
適当に４つの動物を登録した状態

## 手順5
AnimalFormを開く
http://127.0.0.1:8000/animal/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/dbcb698a-68d1-d709-68d3-0fd449ada5fc.png)
先ほど管理画面で登録した４種類の動物が確認できる

## 手順6
管理画面に戻りanimalモデルを追加する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/20871014-e039-456b-66df-75e4a70cdc20.png)
猿とライオンを追加して計6種になった

## 手順7
AnimalFormの画面に戻り、CTRL+F5で再読み込みする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/3d82b280-a8bc-7386-be1c-575a7658a901.png)
フォームでも追加した猿とライオンが確認できる
動的に選択フォームを更新できた

## 手順8
フォームを選択し、POSTボタンを押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/209f9484-cfac-8078-e864-0c061fc5b7e8.png)
複数選択した動物が取得できていることを確認して終わり

## 解説
`views.py`の`get_form_kwargs`を使い、クラスベースビューからフォームクラスへ値を渡している
`views.py`は画面が読み込まれるたびに呼ばれるため、その時の最新の`AnimalModel`を取得しに行く
`products = AnimalModel.objects.values_list('animal', flat=True)`の部分が該当
フォームクラスでは`__init__`を使い、ビュークラスで設定した辞書型のキーを引数として受け取っている

このあたりの処理を`views.py`ではなく`forms.py`に記載しても冒頭に記載したようにもう1度`manage.py runserver`されるまで更新されなかった
シェルスクリプト等で定期的に`manage.py runserver`させる手もあったが、この手法で必要なくなった