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

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

さて、前回はConsumerの大まかな部分をつくりましたので実際の処理をConsumerにいれていき、カンバンが表示できるようにしていきます。

### Consumerへの処理の組み込み

Consumerにボード全体のデータを戻すメソッドを実装していきます。


#### ボードに必要なモデルの追加

今は`Board`モデルだけが実装されていますが、ボードを表現するには`PipeLine`と`Card`のモデルが足りないので先に作ります。

![KANBAN_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/44722/b77ee783-6f69-33d0-be1e-56ac1915ec43.png)

```python:application/modules/kanban/models/pipe_line.py
from django.db import models


class PipeLine(models.Model):

    board = models.ForeignKey('kanban.Board', on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    order = models.IntegerField(default=0)
    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.board)

```


```python:application/modules/kanban/models/pipe_line.py
from django.db import models


class Card(models.Model):

    pipe_line = models.ForeignKey('kanban.PipeLine', on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    content = models.TextField(null=True, blank=True)
    order = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return '{}: {} of {}'.format(self.pk, self.title, self.pipe_line)
```

CardもPipeLineも並び替えができるように表示順を管理するための`order`という属性をもたせておきます。さらに、`__init__.py`に追記します。

```diff:application/modules/kanban/models/__init__.py
@@ -1 +1,3 @@
 from .board import Board
+from .pipe_line import PipeLine
+from .card import Card
```

では、マイグレーションを行います。

```bash
$ docker-compose exec service python manage.py makemigrations kanban
$ docker-compose exec service python manage.py migrate 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/0002_auto_20180923_0436.py
    - Create model Card
    - Create model PipeLine
    - Add field pipe_line to card
$ docker-compose exec service python manage.py migrate kanban
WARNING: The DJANGO_ENV variable is not set. Defaulting to a blank string.
Operations to perform:
  Apply all migrations: kanban
Running migrations:
  Applying kanban.0002_auto_20180923_0436... OK
```

これでモデルができました。ダミーデータを作りたいのでDjango Afminに登録もしておきます。

```diff:application/modules/kanban/admin.py
@@ -1,7 +1,7 @@
 from django.contrib import admin

-from .models import Board
+from .models import Board, PipeLine, Card

 admin.site.register(Board)
+admin.site.register(PipeLine)
+admin.site.register(Card)
```

http://localhost:3000/admin/ にアクセスすれば、以下のようにPipeLineとCardがメニューに追加されているはずです。

<img width="607" alt="Site_administration___Django_site_admin-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/104848ec-3590-63dd-8dfb-97e6278ef1fd.png">

次の作業のためにダミーデータとしてPipeLineを1つ、Cardを2つ作っておきましょう。

* PipeLine追加
<img width="848" alt="Add_pipe_line___Django_site_admin.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/a7961a3d-4d87-9c85-4843-79754f07511f.png">

* Card追加
<img width="841" alt="Add_card___Django_site_admin.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/b2cc2c65-6345-4dc4-81be-1e9d1a43fb74.png">

<img width="841" alt="Add_card___Django_site_admin-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/edae6737-1d2c-210e-b6cc-ca9124793728.png">


#### ボード全体のデータを戻すメソッドの実装

続いてボードの構成情報をJSONで戻すメソッドを実装します。最終的に以下のようなデータを戻すようにします。

```python:最終的なデータ形式
{
    'board_id': 1,
    'name': 'board name',
    'pipe_line_list': [
        {
            'pipe_line_id': 1,
            'name': 'PipeLine Name1',
            'card_list': [
                {
                    'card_id': 1,
                    'title': 'card1 title',
                    'content': 'content of card1',
                },
                {
                    'card_id': 2,
                    'title': 'card2 title',
                    'content': 'content of card2',
                },
            ],
        },
    ],
}
```

基本的な流れとしては、あるBoardを取得し、そのボードに紐づくPipeLineの一覧を取得し、各PipeLineに紐づくCardの一覧を取得していく形になります。そのため必要なデータを取得するためのメソッドを各モデルに追加していきます。

まずは、ID指定で`Board`が取得できるようにする`get_by_id`メソッドを追加します。

```diff:application/modules/kanban/models/board.py
@@ -15,3 +15,10 @@ class Board(models.Model):
     @classmethod
     def get_list_by_owner(cls, owner):
         return list(cls.objects.filter(owner=owner).order_by('updated_at'))
+
+    @classmethod
+    def get_by_id(cls, board_id):
+        try:
+            return cls.objects.get(pk=board_id)
+        except cls.DoesNotExist:
+            return None
```

続いて、`PipeLine`に、あるBoardに紐づくものすべての`PipeLine`を戻すメソッド(`get_list_by_board`)を追加します。並び替えに対応するため、`order_by('order')`も付与しておきます。

```diff:application/modules/kanban/models/pipe_line.py
@@ -11,3 +11,7 @@ class PipeLine(models.Model):

     def __str__(self):
         return '{}: {} of {}'.format(self.pk, self.name, self.board)
+
+    @classmethod
+    def get_list_by_board(cls, board):
+        return list(cls.objects.filter(board=board).order_by('order'))
```

最後は`Card`に、特定の`PipeLine`に紐づく`Card`を一括で戻すメソッドを追加します。同じ様に`order_by('order')`も付与します。

```diff:application/modules/kanban/models/card.py
@@ -12,3 +12,7 @@ class Card(models.Model):

     def __str__(self):
         return '{}: {} of {}'.format(self.pk, self.title, self.pipe_line)
+
+    @classmethod
+    def get_list_by_pipe_line(cls, pipe_line):
+        return list(cls.objects.filter(pipe_line=pipe_line).order_by('order'))
```

これでモデルの部分は用意できたので、サービスメソッドを実装します。

```diff:application/modules/kanban/service.py
@@ -1,4 +1,4 @@
-from .models import Board
+from .models import Board, Card, PipeLine


 def get_board_list_by_owner(owner):
@@ -16,3 +16,36 @@ def add_board(owner, board_name):
         name=board_name
     )
     return board
+
+
+def get_board_data_by_board_id(board_id):
+    """
+    borad and pipeline and card.
+    :param int board_id:
+    :return:
+    :rtype: dict
+    """
+    # ボード取得
+    board = Board.get_by_id(board_id)
+    board_data = {
+        'board_id': board.id,
+        'name': board.name,
+        'pipe_line_list': []
+    }
+    # ボードに紐づく各パイプライン取得
+    for pipe_line in PipeLine.get_list_by_board(board):
+        pipe_line_data = {
+            'pipe_line_id': pipe_line.id,
+            'name': pipe_line.name,
+            'card_list': []
+        }
+        # パイプラインに紐づくカードを取得
+        for card in Card.get_list_by_pipe_line(pipe_line):
+            pipe_line_data['card_list'].append({
+                'card_id': card.id,
+                'title': card.title,
+                'content': card.content,
+            })
+        board_data['pipe_line_list'].append(pipe_line_data)
+
+    return board_data
```

簡単に動作確認をしておきます。Djangoでは`manage.py shell`で実装したメソッドなどが試せるので以下の様に`get_board_data_by_board_id`を実行してみます。

```bash:動作確認
$ docker-compose exec service ./manage.py shell  # docker内でmanage.py shellを起動
>>> from pprint import pprint
>>> from modules.kanban.service import get_board_data_by_board_id
>>> pprint(get_board_data_by_board_id(1))
{'board_id': 1,
 'name': 'MyBoard1',
 'pipe_line_list': [{'card_list': [{'card_id': 1,
                                    'content': 'content of Card1',
                                    'title': 'Card1'},
                                   {'card_id': 2,
                                    'content': 'content of card2',
                                    'title': 'Card2'}],
                     'name': 'MyList1',
                     'pipe_line_id': 1}]}
```

データの中身などは少し違うかもしれませんが、大体このような形式になっていれば大丈夫です。


#### Consumerへの組み込み

WebSocektでConsumerに接続した際にボードデータを初期値として戻すように組み込んでいきましょう。

```diff:application/views/ws/kanban_consumer.py
@@ -1,5 +1,7 @@
 from channels.generic.websocket import JsonWebsocketConsumer

+from modules.kanban import service as kanban_sv
+

 class KanbanConsumer(JsonWebsocketConsumer):

@@ -9,14 +11,25 @@ class KanbanConsumer(JsonWebsocketConsumer):
         self.consumer_id = id(self)
         # 後で使う
         self.user = None
+        self.board_id = None

     def connect(self):
         # 認証チェック
         if not self.scope['user'].is_authenticated:
             self.close()
             return

         self.user = self.scope['user']
+        self.board_id = self.scope['url_route']['kwargs']['board_id']
         # 接続を受け入れる
         self.accept()
+        # 初期データをクライアントに返送
+        self.send_board_data()
+
+    def send_board_data(self):
+        board_data = kanban_sv.get_board_data_by_board_id(self.board_id)
+        self.send_json({
+            'boardData': board_data
+        })
+        print(board_data)
+
```

`self.scope['url_route']['kwargs']['board_id']`はURLのマッピングルールで`'ws/boards/<int:board_id>'`の`boad_id`にマッチした情報を取得するためのものです。この箇所でURLに含まれているBoardのIDを取得しています。

さらに、先程作ったget_board_data_by_board_idを利用して取得したボードの構成情報をクライアントに戻す`send_board_data`メソッドを定義し、`connect`内で呼び出すようにしています。

#### 動作チェック

[前回](https://qiita.com/denzow/items/45606026ca8269917124#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)作成した`wstest.py`を少し書き換えて使います。

```diff:wstest.py
from wsrequests import WsRequests

wsr = WsRequests()

# login django
wsr.get('http://localhost:3000/accounts/login/')
wsr.post(
    'http://localhost:3000/accounts/login/',
    data={
        'username': 'test',  # Djangoのユーザ名とパスワード
        'password': 'test',
        'csrfmiddlewaretoken': wsr.cookies['csrftoken'],
        'next': '/',
    }
)
wsr.connect('ws://localhost:3000/ws/boards/1')
+ wsr.receive_message()
wsr.disconnect()
```

接続時に返送されるメッセージを受け取るコードに書き換えました。実行して以下の2行目のようにボードの構成データが戻れば成功です。

```bash
$ python wstest.py
2018-09-23 21:16:53,874 - DEBUG - MainThread - connect websocket [ws://localhost:3000/ws/boards/1]
2018-09-23 21:16:53,904 - DEBUG - MainThread - receive message [{"boardData": {"board_id": 1, "name": "MyBoard1", "pipe_line_list": [{"pipe_line_id": 1, "name": "MyList1", "card_list": [{"card_id": 1, "title": "Card1", "content": "content of Card1"}, {"card_id": 2, "title": "Card2", "content": "content of card2"}]}]}}]
2018-09-23 21:16:53,905 - DEBUG - MainThread - disconnect websocket [ws://localhost:3000/ws/boards/1]
```

### 次回

https://qiita.com/denzow/items/3a5da1e1c7b13147f706