※はじめからはこちら
今回はログイン後のホーム画面に作成済のボードの一覧を出すようにしてみます。一覧は別にWebsocketである必要はないので、APIを作ってAjax経由で取得するようにします。流れ自体は前回と近いですが、いよいよカンバンのパーツの作成も入ってきます。
カンバンのボードモデルを定義
まずはボードを表現するモデルを作ります。Djangoのチュートリアルなどではapp
をつくってそこのmodels.py
に列挙していくことが多いですがモデルが増えると少し見通しが悪いので、1モデル1ファイルになるようにしていきます。
kanbanモジュールの作成
まずはカンバン周りのロジックを持つモジュールを作ります。application/modules
配下にkanban
ディレクトリを作り、そのディレクトリに__init__.py
を空で配置します。
$ mkdir application/modules/kanban
$ touch application/modules/kanban/__init__.py
さらにkanbanディレクトリ配下にmodels
ディレクトリと__init__.py
を配置します。
$ mkdir application/modules/kanban/models
$ touch application/modules/kanban/models/__init__.py
このmodels
配下にモデルを作っていきますが、その前にsettings
にkanban
をDjangoのAppとして追加します。
@@ -39,6 +39,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'modules.kanban',
]
Boardモデルの追加
続いて1枚のカンバンを表現するBoard
モデルを追加します。application/modules/kanban/models/board.py
を作成します。
from django.conf import settings
from django.db import models
class Board(models.Model):
# 所有者
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# ボード名
name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return '{}: {} of {}'.format(self.pk, self.name, self.owner)
所有者をボード名以外はとりあえず作成日などのメタ情報をもたせています。続いて、models.Board
のようにアクセスできるよにするためapplication/modules/kanban/models/__init__.py
を変更します。これをしない場合はmodels.board.Board
のようにしかアクセスできず、Djangoでもうまくマイグレーションされません。
@@ -1 +1 @@
-# coding: utf-8
+from .board import Board
マイグレーションの実行
それでは作成したBoardをバックエンドのDBに反映します。今回のチュートリアルではDocker内部でDjangoを動かしているのでdockerコマンド経由で実行します。まずはmakemigrations
でマイグレーションファイルを作成します。
$ docker-compose exec service python manage.py makemigrations kanban
$ docker-compose exec service python manage.py makemigrations kanban
WARNING: The DJANGO_ENV variable is not set. Defaulting to a blank string.
Migrations for 'kanban':
application/modules/kanban/migrations/0001_initial.py
- Create model Board
続いてマイグレーションを行います。
$ docker-compose exec service python manage.py migrate
これでkanban_board
テーブルが作成されました。
adminへの登録
Board
モデルをAdminから変更できるようにしておきます。kanban
モジュール配下にadmin.py
を作成します。
from django.contrib import admin
from .models import Board
admin.site.register(Board)
これでhttp://localhost:3000/admin/ にアクセスし、以下のようにBoardの変更画面が出ていれば完了です。なおログインを求められた場合、チュートリアル通りに進められている場合はtest/testでログインできます。
Boardの追加
この後の作業にあたり、Boardがいくつかある方が便利なので、Django Adminから作ってしまいます。Boards
の右のAdd
を押下し、適当な名前で2個ほど作っておきます。
ボードの一覧を戻すAPIの実装
サービスレイヤーの実装
いよいよ、ボードを戻すAPIの実装に入っていきます。まずはあるユーザが保持するボードの一覧を戻すORMを用意します。ORMはモデルクラスに書いてしまうほうが整理しやすいので、以下のようにapplication/modules/kanban/models/board.py
を変更します。
@@ -11,3 +11,7 @@ class Board(models.Model):
def __str__(self):
return '{}: {} of {}'.format(self.pk, self.name, self.owner)
+
+ @classmethod
+ def get_list_by_owner(cls, owner):
+ return list(cls.objects.filter(owner=owner).order_by('updated_at'))
これでBoard.get_list_by_owner(user)
としたときにuser
が保持するものだけのBoard
のインスタンスのリストが戻されます。apiはviews
配下に作りますが、そこで直接Boardクラスを参照すると、今後の変更時に影響範囲が広くなり望ましくないのでインターフェースとして機能する関数を定義します。新たにapplication/modules/kanban/service.py
を以下の様に作ります。
from .models import Board
def get_board_list_by_owner(owner):
return Board.get_list_by_owner(owner=owner)
API側はget_board_list_by_owner
を呼び出すようにします。
viewの実装
それではAPIとして使うviewを実装します。application/views/api/boards.py
を新たに以下のように作成します。
from django.http import JsonResponse
from django.views.generic import View
from modules.kanban import service as kanban_sv
class BoardListApi(View):
def get(self, request):
"""
ボードの一覧を戻す
"""
board_list = []
for board in kanban_sv.get_board_list_by_owner(request.user):
board_list.append({
'id': board.id,
'name': board.name,
})
return JsonResponse({
'board_list': board_list,
})
各Boardのid
とname
をJSONで返しています。
urls.pyへの割当
作成したViewにアクセスできるようにurls.py
を変更します。
@@ -7,6 +7,7 @@ from django.views.generic import TemplateView
from .accounts import signup as signup_view
from .api.accounts import AccountsApi
+from .api.boards import BoardListApi
urlpatterns = [
@@ -14,7 +15,8 @@ urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls),
- path('api/accounts/', AccountsApi.as_view())
+ path('api/accounts/', AccountsApi.as_view()),
+ path('api/boards/', login_required(BoardListApi.as_view())),
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
これで http://localhost:3000/api/boards/ にアクセスすると以下のようなJSONが帰ってくれば成功です。
{"board_list": [{"id": 1, "name": "MyBoard1"}, {"id": 2, "name": "MyBoard2"}]}
ボードリストを表示
APIができたので、これを利用して画面にボードの一覧を出せるようにしていきます。
KanbanClientの変更
前回から作っているkanbanClient.js
で今回のAPIを利用するために追記します。
@@ -48,6 +48,12 @@ class KanbanClient extends Client {
const response = await this._get(`${this.baseUrl}/accounts/`);
return response.data.accountInfo;
}
+
+ async getBoardList() {
+ const response = await this._get(`${this.baseUrl}/boards/`);
+ return response.data.boardList;
+ }
+
}
これでgetBoardList
を実行すれば、Boardの一覧が取得できます。
Home.vueの実装
http://localhost:3000/
にアクセスすると、いまはヘッダーしかでませんがそこで表示するためのVueコンポーネントを作ります。application/vuejs/src/pages/Home
ディレクトリを作成してから以下のファイルを作ります。
<template>
<div class="home">
<div class="row no-gutters">
</div>
</div>
</template>
<script>
export default {
name: 'home',
};
</script>
<style lang="scss" scoped>
.home {
padding-top: 1rem;
align-items: center;
}
.no-gutters {
margin:1rem;
padding:0;
.col,
[class*="col-"] {
margin: 0;
padding:0.5rem 1rem;
}
}
</style>
中身はまだ殆ど空ですが、とりあえず一旦このままとします。続いて、ルーティングの設定を変更します。
@@ -4,6 +4,7 @@ import WebSocketMiddleware from './middlewares/websocket';
import DefaultLayout from '../components/layouts/DefaultLayout.vue';
import NotFound from '../pages/NotFound.vue';
+import Home from '../pages/Home/Index.vue';
Vue.use(Router);
@@ -15,6 +16,12 @@ const router = new Router({
{
path: '/',
component: DefaultLayout,
+ children: [
+ {
+ path: '',
+ component: Home,
+ },
+ ]
},
{
path: '*',
これで、http://localhost:3000/
へのアクセスでHome.vue
が表示されるようになります。なお、外枠自体は前回つくったヘッダーがちゃんと使われるようになっています。
storeの作成
Homeで使うStoreを定義していきます。VueコンポーネントとStoreのディレクトリ構成が近いほうがわかりやすいので、以下のファイルを作ります。
const state = {
};
const actions = {
};
const mutations = {
};
export default {
namespaced: true,
state,
actions,
mutations,
};
また、メインのStoreに組み込みます。
@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import header from './header';
+import home from './pages/home';
Vue.use(Vuex);
@@ -13,6 +14,7 @@ export default new Vuex.Store({
: [],
modules: {
header,
+ home,
},
state: {
},
sotreへの組み込み
それでは今つくった空っぽのStoreでAPIを利用するようにします。
@@ -1,10 +1,21 @@
+import KanbanClient from '../../../utils/kanbanClient';
+
+
const state = {
+ boardList: [],
};
const actions = {
+ async fetchBoardList({ commit }) {
+ const boardList = await KanbanClient.getBoardList();
+ commit('setBoardList', { boardList });
+ },
};
const mutations = {
+ setBoardList(state, { boardList }) {
+ state.boardList = boardList;
+ },
};
ほとんど前回のアカウント情報取得と変わりませんね。fetchBoardList
のActionを実行すると最終的にstate.boardList
の中にそのユーザが作成したボード一覧が入る形になります。
コンポーネントへのつなぎ込み
それではHome.vue
に今作成したhome.js
のStoreをつなぎます。
<template>
<div class="home">
<div class="row no-gutters">
-
+ {{ boardList }}
</div>
</div>
</template>
<script>
+import { createNamespacedHelpers } from 'vuex';
+
+const { mapState, mapActions } = createNamespacedHelpers('home');
+
export default {
name: 'home',
+ computed: {
+ ...mapState([
+ 'boardList',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'fetchBoardList',
+ ]),
+ },
+ async created() {
+ this.fetchBoardList();
+ },
};
</script>
<style lang="scss" scoped>
これで http://localhost:3000/ にアクセスるととりあえずボードの情報が表示されます。
もう少しまともなデザインにしていきます。
ボードリストのデザインの実装
ボード1つ1つを表現するBoardCard.vue
コンポーネントを作っていきます。
<template>
<router-link :to="boardUrl">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
</div>
</div>
</router-link>
</template>
<script>
export default {
name: 'BoardCard',
props: {
title: {
type: String,
default: '',
},
boardId: {
type: Number,
default: null,
},
},
computed: {
boardUrl() {
return `/boards/${this.boardId}`;
},
},
};
</script>
boardId
とtitle
をPropsとして受け取り、はめ込むだけで特に動きはないコンポーネントです。これをHome.vue
で使うようにします。
@@ -1,18 +1,25 @@
<template>
<div class="home">
<div class="row no-gutters">
- {{ boardList }}
+ <BoardCard class='board-card col-3' v-for="board in boardList"
+ :title="board.name"
+ :boardId="board.id"
+ :key="board.id" />
</div>
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex';
+import BoardCard from './components/BoardCard.vue';
const { mapState, mapActions } = createNamespacedHelpers('home');
export default {
name: 'home',
+ components: {
+ BoardCard,
+ },
computed: {
...mapState([
'boardList',
components
属性に登録することで、他のコンポーネントを利用できるようになります。ここではv-for
を使い、Boardの数だけBoardCard
を表示するようにしています。
これで、アクセスすると以下のようにボードごとにカード風のパネルが表示されるようになっているはずです。
まだクリックした先のURLのマッピングを作っていないので、クリックすると404になりますがそれは後々やっていきます。
次回