6
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でカンバンアプリケーションを作る(4)

Last updated at Posted at 2018-09-19

※はじめからはこちら

今回はログイン後のホーム画面に作成済のボードの一覧を出すようにしてみます。一覧は別に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配下にモデルを作っていきますが、その前にsettingskanbanをDjangoのAppとして追加します。

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を作成します。

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

application/modules/kanban/models/__init__.py
@@ -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を作成します。

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

Boardの追加

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

Add_board___Django_site_admin.png

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

サービスレイヤーの実装

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

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

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のidnameをJSONで返しています。

urls.pyへの割当

作成したViewにアクセスできるようにurls.pyを変更します。

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が帰ってくれば成功です。

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

ボードリストを表示

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

KanbanClientの変更

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

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ディレクトリを作成してから以下のファイルを作ります。

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>

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

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のディレクトリ構成が近いほうがわかりやすいので、以下のファイルを作ります。

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

const actions = {
};

const mutations = {
};


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

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

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を利用するようにします。

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をつなぎます。

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

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

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

ボード1つ1つを表現するBoardCard.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>

boardIdtitleをPropsとして受け取り、はめ込むだけで特に動きはないコンポーネントです。これをHome.vueで使うようにします。

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

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

次回

6
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
6
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?