【Django＋Vue.js】CRUD機能をaxiosでAjax実装

## 概要
DjangoでVue.jsを使ってAjax実装したCRUDのTodoサンプルアプリを作りました。

スマートフォンやタブレットを使って素早く操作したいとき、同期通信だと画面の再読み込みで使い勝手が非常に悪いため、Ajaxを実装することにしました。

DjangoでのAjax実装にあたり、以下のページがとても参考になりました！
参考：[DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話](https://qiita.com/j54854/items/e59a0e0628c7003c69b0)

この記事では、簡単なCRUDサンプルをテーブルで表示したサンプルコードを紹介します。

## 環境
Python 3.8.0
Django 3.0.5

## Github
サンプルコードはGithubのページに公開しています。
Readmeにはデモムービーもつけているので、参考にしていただければと思います。

[Githubのページ](https://github.com/mokocat/sample-app-of-django-vuejs-axios-ajax)

## 作成したアプリ
![2020-04-15 (2)-crop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605279/39a44953-ac70-9437-edc6-a3a6a81baadb.png)

今回作成したアプリです。
Item名と日付のみで登録します。
Editボタンでテーブルの更新ができ、Delete（ゴミ箱）ボタンで削除できます。
一番左のレボタンで、Todoの完了・未完の切り替えができます。
※Categoryは別ページに登録画面を作っていますが、記事内ではコードを省略しています

## やり方
### 使用するModel

```python

# myproject/myapp/models.py
class Category(models.Model):
    category_name = models.CharField(max_length=20, unique=True)
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE
    )

    def __str__(self):
        return self.category_name


class TodoItem(models.Model):
    item = models.CharField(max_length=50)
    item_date = models.DateField(default=datetime.date.today)
    completed = models.BooleanField(default=False)
    category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL)
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE
    )

    def __str__(self):
        return self.item
```
使用するModelには、TodoItemテーブルと、外部キー（FOREIGN KEY）制約のあるCategoryテーブルの2つを用意しています。

### Formの編集

```python

# myproject/myapp/forms.py
class CreateItemForm(forms.ModelForm):
    class Meta:
        model = TodoItem
        fields = ['item', 'item_date']


class CreateCategoryForm(forms.ModelForm):
    class Meta:
        model = Category
        fields = ["category_name"]
```
TodoItemの登録とCategoryの登録フォームを作成しました。

### template(html)の作成
少し長いので、htmlとscriptの箇所で分けて記載します。
#### base.htmlの編集

```html

<!-- myoroject/templates/base.html -->
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

    <!-- Vue.js, axios -->
    <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
```
vue.jsとaxiosのみ導入したい場合は、下3行だけ追加すれば大丈夫です。
今回作ったアプリでは、それ以外にbootstrapを使ったり、moment.jsでDB登録時に日付のフォーマットを変更したりしています。

#### show_todo_items.htmlの作成（html箇所）
```vuejs

<!-- myproject/templates/show_todo_items.html -->
{% extends 'base.html' %}`{% block title %}Todo List{% endblock %}
{% block header %}
    <h1 xmlns:v-bind="http://www.w3.org/1999/xhtml">Todo List</h1>
    {% if user.is_authenticated %}
        <p>{{ user.get_username }} is logged in</p>
    {% endif %}
{% endblock header %}
{% block content %}

    <div id="todo_list">
<!-- Create Todo Form -->
        <form @submit.prevent="addItem">
            <label for="item">Item:</label>
            <input type="text" id="item" v-model="new_item" autocomplete="off">
            <label for="item_date">Date:</label>
            <input type="date" id="item_date" v-model="its_date">
            <button type="submit">Add</button>
        </form>
        <hr>
<!-- Todo Table -->
        <table class="table table-bordered">
            <thead>
            <tr class="table-primary">
                <th style="text-align: center;">レ</th>
                <th style="text-align: center;">Item</th>
                <th style="text-align: center;">Date</th>
                <th style="text-align: center;">Category</th>
                <th style="text-align: center;">Edit</th>
                <th style="text-align: center;">Delete</th>
            </tr>
            </thead>
            <tbody v-cloak v-for="(item, index) in items">
            <tr class="table-secondary" style="text-decoration: line-through" v-if="item.completed">
                <td @click="changeStatus(item)"><img src="/media/check.png" style="max-height:20px"/></td>
                <td v-text="item.item"></td>
                <td v-text="item.item_date"></td>
                <td v-if="item.category_name" v-text="item.category_name"></td>
                <td v-else>-</td>
                <td>-</td>
                <td @click="deleteItem(item, index)"><img src="/media/delete.png" style="max-height:20px"/></td>
            </tr>
            <tr class="table-info" v-else-if="!item.edit">
                <td @click="changeStatus(item)"><img src="/media/check.png" style="max-height:20px"/></td>
                <td v-text="item.item"></td>
                <td v-text="item.item_date"></td>
                <td v-if="item.category_name" v-text="item.category_name"></td>
                <td v-else>-</td>
                <td @click="item.edit = true"><img src="/media/edit.png" style="max-height:20px"/></td>
                <td @click="deleteItem(item, index)"><img src="/media/delete.png" style="max-height:20px"/></td>
            </tr>
            <tr class="table-info" v-else>
                <td><img src="/media/check.png" style="max-height:20px"/></td>
                <td><input type="text" v-model="item.item"></td>
                <td><input type="date" v-model="item.item_date"></td>
                <td>
                    <div class="form-group">
                        <select class="form-control" id="exampleFormControlSelect1" v-model="item.category_name">
                            <option v-for="category in categories" :value="category.category_name">[[ category.category_name ]]</option>
                        </select>
                    </div>
                </td>
                <td @click="item.edit = false; edit(item)"><img src="/media/edit.png" style="max-height:20px"/></td>
                <td @click="deleteItem(item, index)"><img src="/media/delete.png" style="max-height:20px"/></td>
            </tr>
            </tbody>
        </table>
    </div>
    <script>
    // scriptの内容は下記に記述 
    </script>
{% endblock content %}
```
todo tableのtbody部分は（あまりスマートではないかもしれませんが・・・）、以下の3層構造に分けています。

- v-if="item.completed" ... Itemのcompleted。取り消し線を引く。Editは不可。
- v-else-if="!item.edit"... Itemが未完で編集モードでない。
- v-else... Itemが未完で編集モード。Inputタグで編集可とする。

#### show_todo_items.htmlの作成（script箇所）
次に、Scriptの箇所です。

```vuejs

<!-- myproject/templates/show_todo_items.html -->
    <script>
        Vue.directive('focus', {
            inserted: function (el) {
                el.focus()
            }
        })
        var vm = new Vue({
            delimiters: ['[[', ']]'],
            el: '#todo_list',
            data: {
                items: [],
                categories: [],
                new_item: "",
                its_date: moment(new Date().toLocaleString()).format('YYYY-MM-DD'),
            },
            mounted: function () {
                axios.get(`{% url "get_item" %}`)
                    .then(function (response) {
                        for (var d in response.data.categories){
                            var category = response.data.categories[d];
                            vm.categories.push(category);
                        }
                        for (var d in response.data.items) {
                            var item = response.data.items[d];
                            item.item_date = moment(new Date(item.item_date)).format('YYYY-MM-DD');
                            item['edit'] = false;

                            if (item.category_id){
                                category_name = vm.categories.filter(function (i) {
                                    return i.id === item.category_id;
                                });
                                item['category_name'] = category_name[0].category_name;
                            } else {
                                item['category_name'] = null;
                            }
                             vm.items.push(item);
                        }
                    })
                    .catch(function (error) {
                        console.log(error);
                    })
                    .then(function () {
                    });
            },
            methods: {
                changeStatus: function (item) {
                    item.completed = !item.completed;
                    this.updateDB(item)
                },
                addItem: function () {
                    if (this.new_item && this.its_date) {
                        item = {
                            "item": this.new_item,
                            "item_date": moment(new Date(this.its_date)).format('YYYY-MM-DD'),
                            "completed": false
                        };
                        this.addDB(item);
                    }
                    this.new_item = "";
                },
                edit: function (item) {
                    if (item.category_name) {
                        category_obj = this.categories.filter(function (element){
                            return element.category_name === item.category_name;
                        });
                        item.category_id = category_obj[0].id;
                    }
                    this.updateDB(item)
                },
                 deleteItem: function(item, index){
                   this.items.splice(index, 1);
                   var delete_id = item.id;
                   this.deleteDB(delete_id);
                 },
                addDB: function (item) {
                    csrftoken = Cookies.get('csrftoken');
                    headers = {'X-CSRFToken': csrftoken};
                    data = {
                        "item": item.item,
                        "item_date": moment(new Date(item.item_date)).format('YYYY-MM-DD'),
                        "completed": item.completed,
                    };
                    axios.post('{% url 'post_update_item' %}', data, {headers: headers})
                        .then(function (response) {
                            response.data['edit'] = false;
                            vm.items.push(response.data);
                            console.log(response);
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                },
                updateDB: function (item) {
                    csrftoken = Cookies.get('csrftoken');
                    headers = {'X-CSRFToken': csrftoken};
                    data = {
                        "item_id": item.id,
                        "item": item.item,
                        "item_date": moment(new Date(item.item_date)).format('YYYY-MM-DD'),
                        "completed": item.completed,
                        "category_id": item.category_id,
                    };
                    axios.post('{% url 'post_update_item' %}', data, {headers: headers})
                        .then(function (response) {
                            console.log(response);
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                },
                deleteDB: function (delete_id) {
                    csrftoken = Cookies.get('csrftoken');
                    headers = {'X-CSRFToken': csrftoken};
                    data = {
                        "item_id": delete_id
                    };
                    axios.post('{% url 'post_delete_item' %}', data, {headers: headers})
                        .then(function (response) {
                            console.log(response);
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                }
            }
        })
    </script>
```
参考にしている記事のコードと同様の部分も多いです。
TodoItemと外部キー（Forign Key)制約のあるCategoryの両方をテーブルで表示させたいため、axios.getで両方の要素を取得しています。

```javascript

    if (item.category_id){
        category_name = vm.categories.filter(function (i) {
        return i.id === item.category_id;
    });
    item['category_name'] = category_name[0].category_name;
```
また、TodoItemのcategoryはcategory_idとして取得されているので、category_idで取得した'category_name'を、items[ ]に追加しました。
さらに、items[ ]には、'edit'キーも追加しています。

```javascript

    edit: function (item) {
        if (item.category_name) {
            category_obj = this.categories.filter(function (element){
            return element.category_name === item.category_name;
            });
            item.category_id = category_obj[0].id;
        }
```
editメソッドでは、入力したcategoryはcategory_nameとして取得されるので、これをまたcategory_idにして、updateDBメソッドに渡しています。

```javascript

    axios.post('{% url 'post_update_item' %}', data, {headers: headers})
        .then(function (response) {
        response.data['edit'] = false;
        vm.items.push(response.data);
```
addDBメソッドで、post後のresponse dataをitems[ ]に追加して、html上に表示しています。

### views.pyの編集
次にviews.pyをみてみます。

```python

# myproject/myapp/views.py
class ShowItemView(generic.TemplateView):
    template_name = "show_todo_items.html"


def get_item(request):
    items = TodoItem.objects.filter(user=request.user).order_by("completed", "item_date", "category").values()
    item_list = list(items)
    categories = Category.objects.filter(user=request.user).values()
    category_list = list(categories)
    all_lists = {"items": item_list, "categories": category_list}
    return JsonResponse(all_lists, safe=False)


def post_update_item(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        item = json_dict['item']
        item_date = json_dict['item_date']
        user = request.user
        completed = json_dict['completed']
        if not ('item_id' in json_dict.keys()):
            TodoItem.objects.create(item=item, item_date=item_date, completed=completed, user=user)
            items = TodoItem.objects.filter(user=user, item=item, item_date=item_date, completed=completed).values()
        else:
            item_id = json_dict['item_id']
            items = TodoItem.objects.get(pk=item_id)
            category_id = json_dict['category_id']
            items.item = item
            items.item_date = item_date
            items.completed = completed
            items.category_id = category_id
            items.save()
            items = TodoItem.objects.filter(pk=item_id).values()
        new_item = items[0]
        return JsonResponse(new_item)
    else:
        return HttpResponseServerError()


def post_delete_item(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        item_id = json_dict['item_id']
        item = TodoItem.objects.get(pk=item_id)
        item.delete()
        return JsonResponse(json_dict)
    else:
        return HttpResponseServerError()

```
post_update_itemはテンプレートで書いたaddDB、updateDBどちらからもrequestを受け取っています。
（これもあまり綺麗ではないかもしれませんが、）ifでitem_idがない場合（addDB)とある場合（updateDB）で分岐させています。
addDBの場合は、追加したDBをresponse dataで返して、html上でデータを表示しています。

### urls.pyの編集
一応、urls.pyも書いておきます。

```python

# myproject/myapp/urls.py
urlpatterns = [
    path('', views.ShowItemView.as_view(), name='show_item'),
    path('get_item/', views.get_item, name='get_item'),
    path('post_update_item/', views.post_update_item, name='post_update_item'),
    path('post_delete_item/', views.post_delete_item, name='post_delete_item')
]
```

## まとめ
Djangoでaxiosを使い、Ajax実装したCRUDの例を紹介しました。
私はテーブル編集後のDB更新や、外部キー制約のあるテーブル要素の取得等で、何度かつまづきました・・・
一通り動くコードもGithubにあげていますので、参考になればと思います。