DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話（後編）

# はじめに

[前編](https://qiita.com/j54854/items/e59a0e0628c7003c69b0)では，Ajax通信でサーバ側（のDjango）からクライアント側（のVue.js）にデータをもってくることができた．後編では，逆にクライアント側（のVue.js）からサーバ側（のDjango）にデータを送って必要に応じてデータベースを更新することを試みる．

全体のコードはまとめて[GitHub](https://github.com/j54854/ajaxTest)に置いた．

# DjangoのFormによる新規タスクの追加

最初に，Ajaxを使わずに，クライント側からDjangoのFormでデータを取得する方法を振り返っておこう．そのために，新規タスクを追加することを考える．新規タスクはすべて未完了だと仮定して，次のようなFormを用意した（新たにFormを定義せず，ModelFormを利用する手もあるが）．

```python
from django import forms
import datetime
from . import models

class TodoForm(forms.Form):
    task = forms.CharField(max_length=255)
    due = forms.DateTimeField(
        widget=forms.DateTimeInput(format="%Y-%m-%d"),
        initial = datetime.date.today()
        )

    def save(self):
        new_task = self.cleaned_data["task"]
        its_due = self.cleaned_data["due"]
        if models.Todo.objects.filter(task=new_task).count() == 0:
            todo = models.Todo(task=new_task, due=its_due, done=False);
            todo.save()
```

続いて，このFormを扱うview関数

```python
def add_todo(request):
    if request.method == "POST":
        form = forms.TodoForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("which_url_to_be_redirected_to")
    else:
        form = forms.TodoForm()
    todos = models.Todo.objects.all().order_by("due")
    my_context = {
        "form":form,
        "todos":todos,
        }
    return render(request, "name_of_your_template", my_context)
```

とFormを埋め込むテンプレート

```html
<html>
  <head><title>Todo</title></head>

  <body>
    <h1>My Todo List</h1>
    <div>
      <ul>
        {% for item in todos %}
          <li>
            {% if item.done %}
              <span style="text-decoration: line-through; color: red">
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% else %}
              <span>
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% endif %}
          </li>
        {% endfor %}
      </ul>
      <hr>
      <form method="post" action="{% url 'of_this_page_itself' %}">{% csrf_token %}
        {{ form }}
        <button type="submit" >Add</button>
      </form>
    </div>
  </body>
</html>
```

を作成した．これで，既存タクスのリストの下に新規タスクを追加するための入力フォームと登録ボタンが表示されるようになった．Formを利用したデータの取得は定石的な流れなので，比較的簡単に実装できるようになっている．

ポイントは，フォーム要素（`<form></form>`）の中に埋め込んだ`{% csrf_token %}`である．これを埋め込んでおくだけでDjango側でcsrfの対策が講じられる．逆に，これを埋め込むのを忘れると，DjangoにPOST通信を受け付けてもらえない．

# Vue.jsでの新規タスクの追加

次に，Vue.jsで新規タスクを追加できるようにしてみよう．そのために，前編の最後のテンプレートを下記のように少し拡張する．

```html
<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
      <hr>
      <form v-on:submit.prevent="addTask">
        <label for="task">Task:</label>
        <input type="text" id="task" v-model="new_task">
        <label for="due">Due:</label>
        <input type="date" id="due" v-model="its_due">
        <button type="submit" >Add</button>
      </form>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: [],
    new_task: "",
    its_due: ""
  },
  mounted: function() {
    axios.get('{% url "for_view_function_get_todo()" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
    },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    },
    addTask: function() {
      if(this.new_task && this.its_due){
        this.todos.push({
          "task":this.new_task,
          "due": new Date(this.its_due),
          "done":false
        });
      }
      this.new_task = "";
      this.its_due = "";
    }
  }
})

    </script>
  </body>
</html>
```

上部のhtmlの部分を見てみると，DjangoのFormを利用した場合と同じ箇所にフォーム要素（`<form></form>`）が挿入され，サブミットイベントを処理するディレクティブが追加されていることがわかる．この際に呼び出されるのが`addTask`メソッドである．

この`addTask`メソッドの本体は，下部のVue.jsのスクリプトの中に追加されている．具体的には，`new_task`と`its_due`に情報が入っていることを確認し，それらの情報をもとに新しいtodoのオブジェクトを生成した上で，それを`todos`に追加し，最後に`new_task`と`its_due`を空に戻している．

なお，`new_task`と`its_due`はVueインスタンスの`data`の中に追加されており，それらは`v-model`ディレクティブでフォームに入力されたデータと紐付けてある．これで，Vue.jsの方でも（ページ上で）新規タスクを追加できるようになった．

# Ajax通信（その2，POST編）

ただし，まだデータベースは更新していないので，このままではページをリロードすると新しく追加したタスクは消えてしまう．そこで，前編とは逆に，Vue.js側からDjango側にAjax通信でJSON形式の情報を送り込んで，データベースを更新することを試みよう．ここからが後編の本題である．

このAjaxリクエストを処理するview関数から始めよう．これは次のように構成した．

```python
def post_todo(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        task = json_dict['task']
        due = json_dict['due']
        done = json_dict['done']
        todos = models.Todo.objects.filter(task=task)
        if not todos:
            models.Todo.objects.create(task=task, due=due, done=done);
        else:
            todos[0].due=due
            todos[0].done=done
            todos[0].save()
        return JsonResponse(json_dict)
    else:
        return HttpResponseServerError()
```

POST通信のリクエストから必要なJSONデータを抽出し，pythonの辞書に変換するには，`request.body`を`json.loads()`に渡せばいいらしい．

そして，得られた辞書からタスク（`task`），納期（`due`），完了済みかどうか（`done`）の情報を取り出し，同じ名称のタスクがなければ，それを新しいタスクとして生成しデータベースに格納している．すでに同じ名称のタスクがあった場合は，そのタスクの情報を上書きしている．

一方，Vue.js側のメソッドは，GET通信の場合と同様に考えると，例えば次のようになる．

```javascript
updateDB: function(item) {
  data = {
      "task":item.task,
      "due": new Date(item.due),
      "done":item.done
    };
  axios.post('{% url "for_view_function_post_todo()" %}')
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}
```

しかし残念ながら，このままではDjangoはこのリクエストを受け付けてくれない．これは，後編の最初に書いたcsrf対策のためである．Ajax通信の場合は`{% csrf_token %}`を埋め込むことはできないので，これに変わる方法が必要となる．

詳細はDjangoの公式ドキュメントの[ここ](https://docs.djangoproject.com/en/2.2/ref/csrf/)を参照してほしいが，要するに，Cookieから必要な情報（csrfトークン）を抽出してそれを`X-CSRFToken`という名称でPOST通信のリクエストのヘッダに書き込めばいいということらしい．

公式ドキュメントにはcsrfトークンを抽出するためのjQueryのコードも紹介されているが，ここでもそのためだけにjQueryをロードするのは避けたいので，もうひとつの手段として紹介されているJavaScriptクッキーライブラリを利用することにする．これも次のタグでCDNからロードしておこう．

```html
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
```

これを用いて，上の`updateDB`メソッドを次のように拡張すると無事動くようになった．

```javascript
updateDB: function(item) {
  csrftoken = Cookies.get('csrftoken');
  headers = {'X-CSRFToken': csrftoken};
  data = {
      "task":item.task,
      "due": new Date(item.due),
      "done":item.done
    };
  axios.post('{% url "todo:post_todo" %}', data, {headers: headers})
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}
```

他の部分も含めたテンプレートの全体は次の通りである．

```html
<html>
  <head><title>Todo</title></head>

  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
      <hr>
      <form v-on:submit.prevent="addTask">
        <label for="task">Task:</label>
        <input type="text" id="task" v-model="new_task">
        <label for="due">Due:</label>
        <input type="date" id="due" v-model="its_due">
        <button type="submit" >Add</button>
      </form>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: [],
    new_task: "",
    its_due: ""
  },
  mounted: function() {
    axios.get('{% url "todo:get_todo" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
    },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
      this.updateDB(item);
    },
    addTask: function() {
      if(this.new_task && this.its_due){
        item = {
          "task":this.new_task,
          "due": new Date(this.its_due),
          "done":false
        };
        this.todos.push(item);
        this.updateDB(item);
      }
      this.task = "";
      this.due = "";
    },
    updateDB: function(item) {
      csrftoken = Cookies.get('csrftoken');
      headers = {'X-CSRFToken': csrftoken};
      data = {
          "task":item.task,
          "due": new Date(item.due),
          "done":item.done
        };
      axios.post('{% url "todo:post_todo" %}', data, {headers: headers})
        .then(function (response) {
          console.log(response);
        })
        .catch(function (error) {
          console.log(error);
        });
    }
  }
})

    </script>
  </body>
</html>
```

よく見るとツッコミどころはたくさんありそうだけど，ひとまず目標は達成できたのでよしとする．


# まとめ

[前編](https://qiita.com/j54854/items/e59a0e0628c7003c69b0)，後編を通して，DjangoのテンプレートにVue.jsを導入してちょこっと使ってみるための基礎はできたと思う．Vue.js自体の機能はまだほんの触りしか使っていないので，これからいろいろ試してみたい．

ここまで読んでくださった方に感謝．　ｍ（＿＿）ｍ