概要
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にはデモムービーもつけているので、参考にしていただければと思います。
作成したアプリ
今回作成したアプリです。
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にあげていますので、参考になればと思います。