4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-15

概要

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

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

DjangoでのAjax実装にあたり、以下のページがとても参考になりました!
参考:DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話

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

環境

Python 3.8.0
Django 3.0.5

Github

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

Githubのページ

作成したアプリ

2020-04-15 (2)-crop.png

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

やり方

使用するModel


# 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の編集


# 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の編集


<!-- 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箇所)


<!-- 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の箇所です。


<!-- 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で両方の要素を取得しています。


    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'キーも追加しています。


    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メソッドに渡しています。


    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をみてみます。


# 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も書いておきます。


# 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にあげていますので、参考になればと思います。

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?