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

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

前回で個別のカード周りはできたので、今回はパイプライン周りの更新などを実装していきます。

### パイプラインの追加・更新のバックエンドの実装

新しいパイプラインを追加するためのサーバサイドのロジックを追加していきます。

#### モデル層の変更

パイプラインの追加時には、新しい`PipeLine`モデルの作成をしますが、その際にはどこに追加するかを決める`order`属性を設定します。今回は右端に追加したいので、追加時点のパイプラインの数も取得できるようにしておきます。

```diff:application/modules/kanban/models/pipe_line.py
@@ -12,6 +12,10 @@ class PipeLine(models.Model):
     def __str__(self):
         return '{}: {} of {}'.format(self.pk, self.name, self.board)

+    @classmethod
+    def create(cls, **params):
+        return cls.objects.create(**params)
+
     @classmethod
     def get_list_by_board(cls, board):
         return list(cls.objects.filter(board=board).order_by('order'))
@@ -22,3 +26,7 @@ class PipeLine(models.Model):
             return cls.objects.get(id=pipe_line_id)
         except cls.DoesNotExist:
             return None
+
+    @classmethod
+    def get_current_pipe_line_count_by_board(cls, board):
+        return cls.objects.filter(board=board).count()
```

#### サービスの実装

追加したORMのメソッドを使ってパイプライン周りのサービスメソッドを作ります。

```diff:application/modules/kanban/service.py
@@ -106,3 +106,31 @@ def update_card(card_id, title=None, content=None):
 def delete_card(card_id):
     card = Card.get_by_id(card_id)
     card.delete()
+
+
+def add_pipe_line(board_id, pipe_line_name):
+    board = Board.get_by_id(board_id)
+    current_count = PipeLine.get_current_pipe_line_count_by_board(board)
+    return PipeLine.create(
+        board=board,
+        name=pipe_line_name,
+        order=current_count + 1,
+    )
+
+
+def update_pipe_line(pipe_line_id, name=None):
+    """
+    :param int pipe_line_id:
+    :param str name:
+    :return:
+    """
+    pipe_line = PipeLine.get_by_id(pipe_line_id)
+    if name:
+        pipe_line.name = name
+    pipe_line.save()
+    return pipe_line
+
+
+def delete_pipe_line(pipe_line_id):
+    pipe_line = PipeLine.get_by_id(pipe_line_id)
+    pipe_line.delete()

```

`add_pipe_line`だけ上述のように、同じボードに所属しているパイプラインの数を取得した上で`order`属性にセットすることで右端へのパイプライン追加を行っています。

なお、CardモデルはPipeLineに対してのFKで`on_delete=models.CASCADE`を指定しているので紐づくPipeLineを削除するとあわせて削除されます。

```python

class Card(models.Model):

    pipe_line = models.ForeignKey('kanban.PipeLine', on_delete=models.CASCADE)
```

#### consumerへの組み込み

パイプラインへの変更は直ちに他のクライアントへも通知する必要があるので、Websocketで処理をするようにします。そのため、先程のサービスメソッドをもとにConsumerを書き換えていきます。

```diff:application/views/ws/kanban_consumer.py
@@ -16,7 +16,10 @@ class KanbanConsumer(JsonWebsocketConsumer):
         self.namespace = 'board'
         self.action_map = {
             'update_card_order': self.update_card_order,
-            'broadcast_board_data': self.broadcast_board_data
+            'broadcast_board_data': self.broadcast_board_data,
+            'add_pipe_line': self.add_pipe_line,
+            'rename_pipe_line': self.rename_pipe_line,
+            'delete_pipe_line': self.delete_pipe_line,
         }
         self.room_group_name = None

@@ -70,6 +73,35 @@ class KanbanConsumer(JsonWebsocketConsumer):
             }
         )

+    def add_pipe_line(self, content):
+        """
+        パイプラインの追加
+        """
+        board_id = content['boardId']
+        pipe_line_name = content['pipeLineName']
+        kanban_sv.add_pipe_line(board_id, pipe_line_name)
+        self.broadcast_board_data()
+
+    def rename_pipe_line(self, content):
+        """
+        パイプライン名の変更
+        """
+        pipe_line_id = content['pipeLineId']
+        pipe_line_name = content['pipeLineName']
+        kanban_sv.update_pipe_line(pipe_line_id, pipe_line_name)
+        self.broadcast_board_data()
+
+    def delete_pipe_line(self, content):
+        """
+        パイプラインの削除
+        :param content:
+        :return:
+        """
+        board_id = content['boardId']
+        pipe_line_id = content['pipeLineId']
+        kanban_sv.delete_pipe_line(pipe_line_id)
+        self.broadcast_board_data()
+
     def receive_json(self, content, **kwargs):
         """
```

どれも、必要なパラメータを受け取ってサービスメソッドを呼び出し、`broadcast_board_data`で全クライアントへ再取得を依頼するだけです。これでバックエンド側は準備ができました。


### パイプラインの追加・更新をフロントに組み込み

#### storeへの組み込み

Consumerの呼び出しなので、`kanbanClient`は特に変更せず、StoreにActionを追加しておきます。

```diff:application/vuejs/src/store/pages/board.js
@@ -77,6 +77,32 @@ const actions = {
     // カード自体はボード自体に出ているので他のクライアントへの反映を依頼する必要がある
     dispatch('broadcastBoardData');
   },
+  addPipeLine({ getters }, { boardId, pipeLineName }) {
+    console.log(boardId, pipeLineName);
+    const socket = getters.getSocket;
+    socket.sendObj({
+      type: 'add_pipe_line',
+      boardId,
+      pipeLineName,
+    });
+  },
+  renamePipeLine({ getters }, { pipeLineId, pipeLineName }) {
+    const socket = getters.getSocket;
+    socket.sendObj({
+      type: 'rename_pipe_line',
+      pipeLineId,
+      pipeLineName,
+    });
+  },
+  deletePipeLine({ getters }, { boardId, pipeLineId }) {
+    console.log(boardId, pipeLineId);
+    const socket = getters.getSocket;
+    socket.sendObj({
+      type: 'delete_pipe_line',
+      boardId,
+      pipeLineId,
+    });
+  },
 };
```

#### パイプライン追加のボタンを追加

カンバンの右端にパイプラインの追加用のボタンを置きます。まずはそれようのコンポーネントを作ります。

```vue:application/vuejs/src/pages/Board/components/BoardArea/AddPipeLine.vue
<template>
  <div class="add-pipe-line">
    <nav class="navbar navbar-dark add-pipe-line-button" @click="addPipeLineAction">
      <span class="navbar-brand mb-0 h1">
        Add List(+)
      </span>
    </nav>
  </div>
</template>

<script>

import { createNamespacedHelpers } from 'vuex';

const { mapActions, mapState } = createNamespacedHelpers('board');


export default {
  name: 'AddPipeLine',
  computed: {
    ...mapState([
      'boardData',
    ]),
  },
  methods: {
    addPipeLineAction() {
      const pipeLineName = window.prompt('PipeLineName?');
      if (pipeLineName) {
        this.addPipeLine({
          boardId: this.boardData.boardId,
          pipeLineName,
        });
      }
    },
    ...mapActions([
      'addPipeLine',
    ]),
  },
};
</script>

<style lang='scss' scoped>
  .add-pipe-line {
    margin-right: 1rem;
    width: 15rem;
  }
  .add-pipe-line-button {
    cursor: pointer;
  }
  .navbar {
    background-color: #8c939d;
  }
</style>

```

`PipeLine.vue`を薄めたようなコンポーネントです。先程用意した`addPipeLine`のActionも割り当てておきます。作成したこの`AddPipeLine`を`BoardArea.vue`に組み込みます。

```diff:application/vuejs/src/pages/Board/components/BoardArea.vue
@@ -11,6 +11,7 @@
         class="pipe-line-item"
         :key="pipeLine.id"
       />
+      <AddPipeLine />
     </Draggable>
     <router-view></router-view>
   </div>
@@ -21,6 +22,7 @@ import Draggable from 'vuedraggable';
 import { createNamespacedHelpers } from 'vuex';

 import PipeLine from './BoardArea/PipeLine.vue';
+import AddPipeLine from './BoardArea/AddPipeLine.vue';

 const { mapGetters } = createNamespacedHelpers('board');

@@ -30,6 +32,7 @@ export default {
   components: {
     Draggable,
     PipeLine,
+    AddPipeLine,
   },
   data() {
```

これで、以下のように追加ボタンが実装されました。

![KANBAN-11.png](https://qiita-image-store.s3.amazonaws.com/0/44722/2c2f0b43-6f1d-5f2a-2299-f917b0aa920c.png)

#### パイプラインの変更削除の組み込み


`PipeLine.vue`にActionを追加し、編集用の画面切り替えも実装します。

```diff:application/vuejs/src/pages/Board/components/BoardArea/PipeLine.vue
@@ -1,13 +1,19 @@
 <template>
   <div class="pipe-line">
     <nav class="navbar navbar-dark">
-      <span>
-        <span class="navbar-brand mb-0 h1 pipe-line-name">{{ pipeLineName }}</span>
+      <span v-show="!isEditingPipeLineName">
+        <span class="navbar-brand mb-0 h1 pipe-line-name"
+              :class="{ 'waiting-rename' : isWaitingRename}"
+              @dblclick="startPipeLineNameEdit">{{ pipeLineName }}</span>
         <span class="navbar-brand delete-pipe-line" data-toggle="tooltip" data-placement="top"
               title="delete pipeline" @click="delPipeLineAction">
           (-)
         </span>
       </span>
+      <span v-show="isEditingPipeLineName">
+        <input type="text" v-model="editPipeLineName">
+        <button type="button" class="btn btn-primary" @click="savePipeLineName">save</button>
+      </span>
     </nav>
     <button class="add-card-button btn btn-block" @click="addCardAction">
       add card
@@ -27,6 +33,7 @@
 </template>

 <script>
+
 import { createNamespacedHelpers } from 'vuex';
 import Draggable from 'vuedraggable';
 import Card from './Card.vue';
@@ -53,6 +60,9 @@ export default {
         animation: 300,
         draggable: '.item',
       },
+      isEditingPipeLineName: false,
+      isWaitingRename: false,
+      editPipeLineName: '',
     };
   },
   computed: {
@@ -72,6 +82,13 @@ export default {
       return this.pipeLine.name;
     },
   },
+  watch: {
+    pipeLine(newPipeLine, oldPipeLine) {
+      if (newPipeLine.name !== oldPipeLine.name) {
+        this.isWaitingRename = false;
+      }
+    },
+  },
   methods: {
     addCardAction() {
       const cardTitle = window.prompt('CardTitle?');
@@ -83,11 +100,31 @@ export default {
       }
     },
     delPipeLineAction() {
-      window.confirm(`DELETE [${this.pipeLineName}] ? Are you sure?`);
+      if (!window.confirm(`DELETE [${this.pipeLineName}] ? Are you sure?`)) return;
+      this.deletePipeLine({
+        boardId: this.getBoardId(),
+        pipeLineId: this.pipeLine.pipeLineId,
+      });
+    },
+    startPipeLineNameEdit() {
+      this.isEditingPipeLineName = true;
+      this.editPipeLineName = this.pipeLine.name;
+    },
+    async savePipeLineName() {
+      this.isEditingPipeLineName = false;
+      if (this.editPipeLineName === this.pipeLine.name) return;
+      await this.renamePipeLine({
+        pipeLineId: this.pipeLine.pipeLineId,
+        pipeLineName: this.editPipeLineName,
+      });
+      // リネーム完了までのフラグ
+      this.isWaitingRename = true;
     },
     ...mapActions([
       'updateCardOrder',
       'addCard',
+      'renamePipeLine',
+      'deletePipeLine',
     ]),
     ...mapGetters([
       'getBoardId',

```

これで以下のように`(-)`をクリックするとパイプラインが削除され、ダブルクリックからのパイプライン名の変更が実装できました。

![](https://i.gyazo.com/5cd91f970deb911f12825fa18cddabaf.gif)
![](https://i.gyazo.com/75d90508fa7eaacb17dcca2156b763be.gif)

正常に動作するようになれば完成です。

### 次回

カードの絞り込みのための検索機能の実装をします。