DjangoでAjax（汎用クラスビューを使用）

『Django Ajax』で検索するといくつか記事がヒットするがほとんどが関数ベースビューによるものだったので、汎用ビューを使用したコードを解説。

汎用ビューよく分かってない方はまずはそちらに入門しましょう。

- [Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル](https://qiita.com/felyce/items/7d0187485cad4418c073)
- [Djangoの 汎用クラスビューをまとめて、実装について言及する](https://qiita.com/renjikari/items/af3e8958d2653e6f8d46)

これは私見ですが、Djangoを採用する大きなメリットの1つがクラスベースである汎用ビューを利用することだと考えています。
~~**たぶんこれがベストプラクティスだと思います**。~~

# 完成品イメージ

GitHub - [skokado/django_ajax: Django + Ajax サンプル](https://github.com/skokado/django_ajax)

入力した文字列を画面上に表示するというもの。

簡単に仕様を決めておく。

- ユーザは`/greet`にブラウザでアクセスする
- フォームに入力した値(`name`)はPOSTメソッドで送信する
    - ※POSTメソッドの送信先パスは画面の表示と同じ`/greet`
- `views.py`ではAjaxリクエスト（XMLHttpRequest）を識別してそれ以外の処理と切り離す
- Ajaxリクエストに対して`こんにちは、[name]さん！`というメッセージを`<p>`タグで画面内に表示する


![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/78f4775c-1e93-72bb-2139-288a1daaaf9b.gif)

# コード解説

コンポーネントは大きく以下。

- `urls.py`: ルーティング（URLバイディング）
- `forms.py`: ユーザがデータを入力するためのフォームを定義
- `views.py`: リクエスト処理を記述。
    - 汎用ビューの`FormView`を使う
    - `Request.is_ajax()`を使用してAjaxリクエストを識別する
- テンプレート（`index.html`）: 表示するHTML。本記事ではjQueryも同梱する。

## 準備

DjangoプロジェクトとDjangoアプリケーションの作成を適当に済ませておく。
※プロジェクト名: `django_ajax`, アプリケーション名: `greeting` とする。

### `urls.py`にルーティングルールを記載。

## `urls.py`

使用するパスは`/greet`のみ。

```python:django_ajax/urls.py
from django.urls import path

from . import views


urlpatterns = [
    path('greet/', views.GreetView.as_view(), name='greet'),
]
```

## `forms.py`

`name`を入力するだけ

```python:greeting/forms.py
from django import forms


class GreetForm(forms.Form):
    name = forms.CharField(label='あなたの名前は？')
```

## `index.html`

- フォーム部分

`<div id="result">`の要素配下に`こんにちは、[name]さん！`という文言が追加されていく。
（と、いう処理をjQueryに記述する）

```html
<h1>Let's Greeting!</h1>
<form action="" method="post">{% csrf_token %}
    {{ form }}
    <button type="submit">送信</button>
</form>

<div id="result">
    <!-- ここにあいさつ文の <p> タグ要素が追加されていく -->
</div>
```

- スクリプト部分

jQuery部分についても上述の仕様通りになっていることが何となく読み取れると思う。
また、(slimでない)jQueryが必要なのでロードする。

```html
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script>
// 送信ボタンにイベントリスナーを設定。内部に Ajax 処理を記述
$("form").submit(function(event) {
    event.preventDefault();
    var form = $(this);
    $.ajax({
      url: form.prop("action"),
      method: form.prop("method"),
      data: form.serialize(),
      timeout: 10000,
      dataType: "text",
    })
    .done(function(data) {
        $("#result").append("<p>" + data + "</p>");
    })
});
</script>
```

<details><summary>上記をがっちゃんこした`index.html`</summary><div>

```html
<h1>Let's Greeting!</h1>
<form action="" method="post">{% csrf_token %}
    {{ form }}
    <button type="submit">送信</button>
</form>

<div id="result">
    <!-- Will be replaced with inputed text by Ajax -->
</div>

<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script>
// 送信ボタンにイベントリスナーを設定。内部に Ajax 処理を記述
$("form").submit(function(event) {
    event.preventDefault();
    var form = $(this);
    $.ajax({
      url: form.prop("action"),
      method: form.prop("method"),
      data: form.serialize(),
      timeout: 10000,
      dataType: "text",
    })
    .done(function(data) {
        $("#result").append("<p>" + data + "</p>");
    })
});
</script>
```
</div></details>

## `views.py`

`FormView`を継承した`GreetView`1つを定義する

```greeting/views.py
from django.http import HttpResponse
from django.views.generic import FormView

from . import forms

# Create your views here.
class GreetView(FormView):
    template_name = 'index.html'  # テンプレート名(htmlファイル名)
    form_class = forms.GreetForm
    success_url = '/greet'

    def post(self, request, *args, **kwargs):
        form = self.get_form(self.form_class)
        if form.is_valid():
            if request.is_ajax():
                """Ajax 処理を別メソッドに切り離す"""
                print('### Ajax request')
                return self.ajax_response(form)
            # Ajax 以外のPOSTメソッドの処理
            return super().form_valid(form)
        # フォームデータが正しくない場合の処理
        return super().form_invalid(form)

    def ajax_response(self, form):
        """jQuery に対してレスポンスを返すメソッド"""
        name = form.cleaned_data.get('name')
        return HttpResponse(f'こんにちは、{name}さん！')
```

### ちなみに

`views.py`内の`if request.is_ajax():`について、公式ドキュメントには以下のようにある。

参照 - [リクエストとレスポンスのオブジェクト | Django ドキュメント | Django](https://docs.djangoproject.com/ja/3.0/ref/request-response/#django.http.HttpRequest.is_ajax)

（はんなり和訳）

```
HttpRequest.is_ajax()

HTTP_X_REQUESTED_WITHヘッダの'XMLHttpRequest'文字列で
リクエストがXMLHttpRequest経由によるものかどうかをチェックし、Trueを返します。
多くのモダンなJavaScriptライブラリはこのヘッダを送信します。
```

つまり、このメソッドはリクエストヘッダの`HTTP_X_REQUESTED_WITH`に`XMLHttpRequest`がセットされる場合にのみ使える。
jQueryはこれをちゃんとセットしてくれるっぽい。
※**jQueryを使わない（素のJavaScript）で少し試したところ上手くいきませんでした**。

- 検証コード

`views.py`の`GreetView`を以下のように書き換えてリクエストヘッダを覗いてみる。

```python
class GreetView(FormView):
    template_name = 'greet.html'
    form_class = forms.GreetForm
    success_url = '/greet'

    def post(self, request, *args, **kwargs):
        # ★追加
        for k, v in request.META.items():
            print(k, v)
        # ...(以下同様)
```

- 結果

※デバッグモードだと大量のリクエストヘッダが全て出力されるので抜粋

```shell
...
HTTP_ACCEPT text/plain, */*; q=0.01
HTTP_X_REQUESTED_WITH XMLHttpRequest  # <= これ
HTTP_USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
...
```

# まとめ

以上、汎用ビューによる Django のAjax サンプルを記載した。
他の記事よりも簡潔に実装できていると思う。
他にも、`StreamingHttpResponse`と組み合わせるとどんなことができるか試してみたい