【Django】ModelFormで選択フォームを動的に更新する方法

### 作りたいもの
学部・学科を選択するフォーム
学部を選択したら、その学部に含まれる学科のみが学科の選択肢に現れるようにしたい。

フロントエンドにRactやVueを用いれば簡単だが、導入コストの問題で今回はDjangoTemplateを使って実装した。

### 方法
学部が選択されたことを検知する必要がある。
- 学部が選択されたら学部idを受け取ってその学部に属する学科の配列を返すAPIを作成する
- 学科の選択を検知してAPIにそれを送信するscriptをjQueryで作成する
- 学科の選択を検知してjQueryでテンプレートを書き換える
以上の3つを実装していきます。

### 環境
Django 4.2
Python 3.10.11

### 実装
#### Model
学部のモデルと学科のモデルを1対多の関連付けで作ります。
実際のModelはこんな感じです。
```python
# models.py
class Faculty(models.Model):
    """
    学部
    """
    faculty = models.CharField('学部名', max_length=20)

    def __str__(self):
        return self.faculty

class Department(models.Model):
    """
    学科
    """
    department = models.CharField('学科名', max_length=20)
    faculty = models.ForeignKey(
        Faculty,
        verbose_name='学部',
        null=True,
        blank=False,
        on_delete=models.PROTECT,
    )

    def __str__(self):
        return self.department

class Question(models.Model):
    """
    学部と学科を選択するModelFormに使う用のモデル
    """
    faculty = models.ForeignKey(
        Faculty,
        verbose_name='学部',
        null=True,
        blank=False,
        on_delete=models.PROTECT,
    )
    department = models.ForeignKey(
        Department,
        verbose_name='学科',
        null=True,
        blank=False,
        on_delete=models.PROTECT,
    )
```

#### 変更を受け取るAPI
変更を受け取るAPIを作ります。`faculty_id`を送って、関連付けされた`department`を返します。
```python
# views.py
def get_departments(request, faculty_id):
    departments = Department.objects.filter(faculty_id=faculty_id).values('id', 'department')
    return JsonResponse(list(departments), safe=False)
```
URLのエンドポイントも設定します。
```python
# urls.py
urlpatterns = [
    path('get_departments/<int:faculty_id>/', views.get_departments, name="get_departments"),
]
```
#### 学部の選択を検知してAPIにそれを送信するscript
テンプレートの書き換えもここでやってます。選択された学部情報をAPIに送信し、返された学科の配列をテンプレートに書き込みます。
```javascript
// send_depatments.js
$(document).ready(function() {
    $('#faculty-select').change(function() {
        var facultyId = $(this).val();
        if (facultyId) {
            console.log(facultyId);
            $.ajax({
                url: '/qanda/get_departments/' + facultyId + '/',
                type: 'GET',
                dataType: 'json',
                success: function(data) {
                    $('#department-select').empty();
                    $('#department-select').append('<option value="">選択してください</option>');
                    $.each(data, function(key, value) {
                        $('#department-select').append('<option value="' + value.id + '">' + value.department + '</option>');
                    });
                }
            });
        } else {
            $('#department-select').empty();
            $('#department-select').append('<option value="">選択してください</option>');
        }
    });
});
```
#### ModelForm
最初は学科の選択フィールドは空にしておきます。jQueryで取得出来るように`__init__`内で`id`を明示的に指定します。
また、Mediaクラスに作成した`send_depatments.js`のパスを指定します。
```python
# forms.py
class QuestionModelForm(forms.ModelForm):
    faculty = forms.ModelChoiceField(queryset=Faculty.objects.all(), empty_label='選択してください')
    department = forms.ModelChoiceField(queryset=Department.objects.none(), empty_label='選択してください')

    class Meta:
        model = Question
        fields = [
            'faculty',
            'department',
        ]

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

        if self.instance.pk:
            self.fields['department'].queryset = self.instance.faculty.department_set.order_by('department')

        self.fields['faculty'].widget.attrs.update({
            'class': 'form-control',
            'id': 'faculty-select',
        })

        self.fields['department'].widget.attrs.update({
            'class': 'form-control',
            'id': 'department-select',
        })

    class Media:
        js = ('js/send_depatments.js',)
```

#### フォームのtemplates
jQueryを使っているので、jQueryを読み込みます。
ModelForm内で指定したMediaを読み込むために`{{ form.media }}`を追加しました。
```html
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<form class="ui form" action="{% url 'qanda:create_question' %}" method="POST">
    {% csrf_token %}
    {{form.as_p}}
    {{ form.media }}
<button class="ui button" type="submit" name="next" value="confirm">送信</button>

```

### おわりに
Djangoの`forms.py`はあくまでHTMLのレンダリングを助けるものなので、以上のように、jQuery等でHTMLを書き換えてしまえば良いというわけです。
Django・jQueryともに初心者ですので誤り・改善点などを見つけられた方がいましたら優しく指摘してくださるとうれしいです。