クラスベースビューにおける form_invalid のときにエラーメッセージを返したいときの処置

# はじめに
Djangoの Class-Based-View でフォームバリデーションの仕組みを構築していたときに沼にハマったのでメモ

# urls
http://www.henojiya.net/ と入力されたら IndexView を呼び出せ

```python:urls.py
app_name = 'shp'
urlpatterns = [
    path('', IndexView.as_view(), name='index'),
```

# views
テンプレートと関連付けるデータを引き連れ、index.htmlを表示させる

```python:views.py
class IndexView(TemplateView):
    # 表示するテンプレート: index.html
    template_name = 'shopping/index.html'

    # テンプレートに引き連れていく各種データやフォーム
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['products'] = Products.objects.all()
        context['form_single'] = SingleRegistrationForm()
        context['form_csv'] = UploadCSVForm()
        context['editablelist'] = Products.objects.order_by('id')[:5]
        context['editableform'] = EditForm()
        return context
```

# CSVをアップロードするためだけのhtml
index.html にはCSVをアップロードするためだけのフォームを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/2b1dadee-7a4c-5dc1-3a9a-44c82456f6dc.png)
formがsubmitされると regist_bulk に飛べと書いてある。

```html:index.html
...省略...
    <h3>CSV登録</h3>
    <form action="{% url 'shp:regist_bulk' %}" method="POST" class="registration" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form_csv.as_p }}
        <input type="submit" value="UPLOAD">
    </form>
...省略...
```

# urls
regist_bulk が呼ばれたら UploadBulkView を呼び出せ

```python:urls.py
app_name = 'shp'
urlpatterns = [
    path('regist/bulk/', UploadBulkView.as_view(), name='regist_bulk'),
```



# views
UploadBulkView が呼ばれると、バリデーションがうまくいったときとうまくいかなかったときの処理を上書きできる仕組みが用意されている。今回はバリデーションでハジかれたときにフォーカスする。この form_invalid に行く前に、フォームのフィールドごとのバリデーションが走る。

```python:views.py
class UploadBulkView(FormView):
    """UploadBulkView"""
    form_class = UploadCSVForm
    success_url = reverse_lazy('shp:index')

    # バリデーションがうまくいったとき
    def form_valid(self, form):
        ...省略...

    # バリデーションがうまくいかなかったとき
    def form_invalid(self, form):
        messages.add_message(self.request, messages.WARNING, form.errors)
        return redirect('shp:index')

```

# フォームのフィールドごとのバリデーション
clean_(field_name)というメソッドをつくるとフィールドごとのバリデーションができる。今回は csv 以外をアップロードしていないかをチェックした。

```python:forms.py
class UploadCSVForm(forms.Form):
    """ formのname 属性が 'file' になる """
    file = forms.FileField(required=True, label='')

    def clean_file(self):
        """csvファイル要件を満たすかどうかをチェックします"""
        file = self.cleaned_data['file']
        if not file.name.endswith('.csv'):
            raise forms.ValidationError('拡張子はcsvのみです')
        return file
```

ValidationError にメッセージを仕込んで raise するとサーバーサイドでのprintをしたときにこんなhtmlが入っていた。

```html:print(ValidationError)
<ul class="errorlist">
  <li>file
    <ul class="errorlist">
      <li>拡張子はcsvのみです</li>
    </ul>
  </li>
</ul>」
```

# views
さて、バリデーションが終わると、form_invalidの処理に流れる。バリデーションが失敗したら、エラーメッセージとともに元のページにリダイレクトするっていうのはよくありそうな処理じゃん？でもリダイレクト時にエラーメッセージを持っていけないのよｗｗｗゴーギャーン！:rage:って感じやん。だから、「form」の引数の「form.errors」に入っているhtmlを messages に渡して、その後にシンプルなリダイレクトという手段を取った。

```python:views.py
class UploadBulkView(FormView):
    """UploadBulkView"""
    form_class = UploadCSVForm
    success_url = reverse_lazy('shp:index')

    # バリデーションがうまくいったとき
    def form_valid(self, form):
        ...省略...

    # バリデーションがうまくいかなかったとき
    def form_invalid(self, form):
        messages.add_message(self.request, messages.WARNING, form.errors)
        return redirect('shp:index')

```

## Tips: 元のページにもどる
```diff_python:views.py
class UploadBulkView(FormView):
    """UploadBulkView"""
    form_class = UploadCSVForm
    success_url = reverse_lazy('shp:index')

    # バリデーションがうまくいったとき
    def form_valid(self, form):
+       return super().form_valid(form)  # あれ、こっちうまくいかねーな...redirectしか無いかな？

    # バリデーションがうまくいかなかったとき
    def form_invalid(self, form):
        messages.add_message(self.request, messages.WARNING, form.errors)
+       return super().form_invalid(form)

```

# 最終結果
htmlにエラーhtmlを表示する記述を追加。とりあえずオッケー！:fist:

```html:index.html
    {% if messages %}
        {% for message in messages %}
        {{ message }}
        {% endfor %}
    {% endif %}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/18376daf-4723-9953-8a3a-6638bc94ed66.png)