DjangoとVueでカンバンアプリケーションを作る(4)

※はじめからは[こちら](https://qiita.com/denzow/items/046f3c8b9bd8d3378eb4#_reference-7b3e4a58510793f8a9c7)

今回はログイン後のホーム画面に作成済のボードの一覧を出すようにしてみます。一覧は別にWebsocketである必要はないので、APIを作ってAjax経由で取得するようにします。流れ自体は[前回](https://qiita.com/denzow/items/f13717d81eaa2e72086e)と近いですが、いよいよカンバンのパーツの作成も入ってきます。


### カンバンのボードモデルを定義

まずはボードを表現するモデルを作ります。Djangoのチュートリアルなどでは`app`をつくってそこの`models.py`に列挙していくことが多いですがモデルが増えると少し見通しが悪いので、1モデル1ファイルになるようにしていきます。

#### kanbanモジュールの作成

まずはカンバン周りのロジックを持つモジュールを作ります。`application/modules`配下に`kanban`ディレクトリを作り、そのディレクトリに`__init__.py`を空で配置します。

```bash
$ mkdir application/modules/kanban
$ touch application/modules/kanban/__init__.py
```

さらにkanbanディレクトリ配下に`models`ディレクトリと`__init__.py`を配置します。

```bash
$ mkdir application/modules/kanban/models
$ touch application/modules/kanban/models/__init__.py
```

この`models`配下にモデルを作っていきますが、その前に`settings`に`kanban`をDjangoのAppとして追加します。

```diff:application/settings/base.py
@@ -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`を作成します。

```python: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でもうまくマイグレーションされません。


```diff:application/modules/kanban/models/__init__.py
@@ -1 +1 @@
-# coding: utf-8
+from .board import Board
```


#### マイグレーションの実行

それでは作成したBoardをバックエンドのDBに反映します。今回のチュートリアルではDocker内部でDjangoを動かしているのでdockerコマンド経由で実行します。まずは`makemigrations`でマイグレーションファイルを作成します。

```bash
$ docker-compose exec service python manage.py makemigrations kanban
```

```bash:実行例
$ 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
```

続いてマイグレーションを行います。

```bash
$ docker-compose exec service python manage.py migrate
```

これで`kanban_board`テーブルが作成されました。


#### adminへの登録

`Board`モデルをAdminから変更できるようにしておきます。`kanban`モジュール配下に`admin.py`を作成します。

```python:application/modules/kanban/admin.py
from django.contrib import admin

from .models import Board

admin.site.register(Board)

```

これでhttp://localhost:3000/admin/ にアクセスし、以下のようにBoardの変更画面が出ていれば完了です。なおログインを求められた場合、チュートリアル通りに進められている場合はtest/testでログインできます。


![Site_administration___Django_site_admin.png](https://qiita-image-store.s3.amazonaws.com/0/44722/51e9c5df-963f-c0e4-421e-2f987dffc21f.png)

### Boardの追加

この後の作業にあたり、Boardがいくつかある方が便利なので、Django Adminから作ってしまいます。`Boards`の右の`Add`を押下し、適当な名前で2個ほど作っておきます。

![Add_board___Django_site_admin.png](https://qiita-image-store.s3.amazonaws.com/0/44722/49e0d158-f0df-9088-7223-a1c03bbea8ef.png)



### ボードの一覧を戻すAPIの実装

#### サービスレイヤーの実装

いよいよ、ボードを戻すAPIの実装に入っていきます。まずはあるユーザが保持するボードの一覧を戻すORMを用意します。ORMはモデルクラスに書いてしまうほうが整理しやすいので、以下のように`application/modules/kanban/models/board.py`を変更します。

```diff: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`を以下の様に作ります。

```python
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`を新たに以下のように作成します。

```python: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`を変更します。

```diff:application/views/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が帰ってくれば成功です。

```json
{"board_list": [{"id": 1, "name": "MyBoard1"}, {"id": 2, "name": "MyBoard2"}]}
```

### ボードリストを表示


APIができたので、これを利用して画面にボードの一覧を出せるようにしていきます。

#### KanbanClientの変更

前回から作っている`kanbanClient.js`で今回のAPIを利用するために追記します。

```diff:application/vuejs/utils/kanbanClient.js
@@ -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`ディレクトリを作成してから以下のファイルを作ります。


```vue:application/vuejs/src/pages/Home/Index.vue
<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>

```

中身はまだ殆ど空ですが、とりあえず一旦このままとします。続いて、ルーティングの設定を変更します。

```diff:application/vuejs/src/router/index.js
@@ -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のディレクトリ構成が近いほうがわかりやすいので、以下のファイルを作ります。

```javascript:application/vuejs/src/store/pages/home.js
const state = {
};

const actions = {
};

const mutations = {
};


export default {
  namespaced: true,
  state,
  actions,
  mutations,
};

```

また、メインのStoreに組み込みます。

```diff:application/vuejs/src/store/index.js
@@ -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を利用するようにします。

```diff:application/vuejs/src/store/pages/home.js
@@ -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をつなぎます。

```diff:application/vuejs/src/pages/Home/Index.vue
 <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/ にアクセスるととりあえずボードの情報が表示されます。

![KANBAN.png](https://qiita-image-store.s3.amazonaws.com/0/44722/c27c013a-ee0e-a834-650c-1d91f17727a6.png)

もう少しまともなデザインにしていきます。

#### ボードリストのデザインの実装

ボード1つ1つを表現する`BoardCard.vue`コンポーネントを作っていきます。

```vue:application/vuejs/src/pages/Home/components/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`で使うようにします。

```diff:application/vuejs/src/pages/Home/Index.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`を表示するようにしています。

これで、アクセスすると以下のようにボードごとにカード風のパネルが表示されるようになっているはずです。


![KANBAN-2.png](https://qiita-image-store.s3.amazonaws.com/0/44722/5299d1e3-0ad4-5039-2ee8-d96c098c2ad7.png)

まだクリックした先のURLのマッピングを作っていないので、クリックすると404になりますがそれは後々やっていきます。

### 次回

https://qiita.com/denzow/items/8409bdde961abfee23d8