Django(SwampDragon)でWebSocket利用したTodo管理サイトを作ってみる

# はじめに
DjangoとAngularJSで利用してWebSocket通信を実装してみようと思いパッケージを探したところ、
DjangoでWebSocket通信するためのツールがすべて揃ってるSwampDragonというものがあったので
定番のTodo管理サイトを作ってみた。

swampdragon: http://swampdragon.net/
github: https://github.com/jonashagstedt/swampdragon 

# SwampDragonの特徴

## WebSocket専用サーバーがある
DjangoのWSGIはHTTPなのでWebSocket通信できないので別サーバーを立てる必要がある。
SwampDragon経由でプロジェクト作成時には、server.py というファイルが生成され
これでサーバーを起動する。(/manage.py runsd.pyでもできる)
サーバーはWebフレームワークのTornadoが動作しており
server.pyの内部では、Connectionするために必要なURL生成して起動させている。

## クライアントとサーバーの通信自体はSockJSを利用してる
server.pyのURLにRouterクラスがマッピングされていて
Routerクラスが保持してるSockJSのconnectionを利用してクライアントへSendしている。

## modelのマッパー機能のがある
Djangoのmodelとredisをシリアリズさせてマッピングしてくれる。
modelのsave時にredisのPub/Subの機能でリアルタイムにクライアントに通知してくれる。

※「pub/sub」
「publish」と「subscribe」の略で日本語では「発行」と「購読」になる。
特定のチャンネルに誰かがイベントを「発行」すると、
そのチャンネルを「購読」している人すべてにそのイベントが通知される。

## クライアントが利用するサービスも用意されてる
クライアントがサーバーにConnectionをはって、データを操作するサービス(js)が
すべて用意されてるので手間があまりかからない。
(サードパーティでObjective-cだけどiOS用のものもあった)

## クライアントから実行できる操作
- データの一覧取得
- データの一覧取得(Pager付き)
- データの単一取得
- データの新規追加
- データの更新
- データの削除
- データの購読
- データの購読解除

## 接続ユーザーのセッションも保持できる
ログインした後のユーザー情報をサーバーサイドで保持できるようになるので
データ取得、更新時などのクエリにユーザーIDなども含めることが可能。
クライアント側ではユーザー情報を持つ必要はなくなる。
セッションを保持するには、SwampDragonの作者が用意してる
SwampDragon-authをパッケージをインストールする必要がある。


# DjangoのSwampDragonを利用してTodo管理サイト作成する

## 実行環境
- Python 2.7.11
- Django 1.8.7
- SwampDragon 0.4.2.2
- SwampDragon-auth 0.1.3

## ディレクトリ構成
ディレクトリ構成は以下のようになってる
appはAngularJS使っていてJSファイルが複数あったけど割愛してる

```bash
application
├── app
│   ├── index.html
│   ├── login.html
├── manage.py
├── module
│   ├── middleware.py
│   └── todo
│       ├── models.py
│       ├── routers.py
│       └── serializers.py
├── server.py
├── settings.py
├── urls.py
└── wsgi.py
```

## settings.py
middlewareとswampdragon用の設定

```py
INSTALLED_APPS = (
    :
    'swampdragon',
    'module.todo',
)

MIDDLEWARE_CLASSES = (
    :
    'module.middleware.AuthenticationMiddleware',
)

# SwampDragon settings
SWAMP_DRAGON_CONNECTION = ('swampdragon_auth.socketconnection.HttpDataConnection', '/data')
DRAGON_URL = 'http://localhost:9999/'
```

## urls.py
Views相当の処理はクライアントのAngularJSで行うのでindex.htmlのみのURLを用意

```py
# -*- coding: utf-8 -*-

from django.conf.urls import include, url
from django.contrib import admin
from django.views.generic import TemplateView

urlpatterns = [
    url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
    url(r'^login', 'django.contrib.auth.views.login', kwargs={'template_name': 'login.html'}, name='login'),
    url(r'^logout', 'django.contrib.auth.views.logout_then_login', kwargs={'login_url': 'login'}, name='logout'),
    url(r'^admin/', include(admin.site.urls)),
]
```

## middleware.py
ログインしていないアクセスはログインページに飛ばすようにする

```py
# -*- coding: utf-8 -*-

from django.shortcuts import redirect


class AuthenticationMiddleware(object):

    def process_request(self, request):
        if request.user.is_authenticated():
            if request.path.startswith('/login'):
                return redirect('/')
        elif request.path == '/':
            return redirect('login')
```

## serializers.py
フロントエンドとデータのやりとりを定義するクラス
- model: モデルを「モジュール名.モデルクラス名」で定義
- publish_fields: クライアントへ通知するカラムの定義
- update_fields: クライアントから更新可能なカラムを定義

```py
from swampdragon.serializers.model_serializer import ModelSerializer


class UserSerializer(ModelSerializer):

    class Meta:
        model = 'auth.User'
        publish_fields = ('username',)


class TodoListSerializer(ModelSerializer):

    class Meta:
        model = 'todo.TodoList'
        publish_fields = ('name', 'description')
        update_fields = ('name', 'description')


class TodoItemSerializer(ModelSerializer):

    class Meta:
        model = 'todo.TodoItem'
        publish_fields = ('todolist_id', 'done', 'name', 'updated_at')
        update_fields = ('todolist_id', 'done', 'name')
```

## models.py
SelfPublishModelを継承させてsave時にPublishが通知されるようにする。
serializer_classでシリアライズ設定したクラスを定義する。

```py
# -*- coding: utf-8 -*-

from django.db import models
from swampdragon.models import SelfPublishModel
from .serializers import TodoListSerializer, TodoItemSerializer


class TodoList(SelfPublishModel, models.Model):
    serializer_class = TodoListSerializer
    user_id = models.IntegerField()
    name = models.CharField(max_length=100)
    description = models.TextField(u'説明', blank=True, null=True)
    created_at = models.DateTimeField(u'作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(u'更新日時', auto_now=True)

    class Meta:
        index_together = ['user_id', 'id']


class TodoItem(SelfPublishModel, models.Model):
    serializer_class = TodoItemSerializer
    user_id = models.IntegerField()
    name = models.CharField(max_length=100)
    todolist_id = models.IntegerField()
    done = models.BooleanField(u'完了フラグ', default=False)
    created_at = models.DateTimeField(u'作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(u'更新日時', auto_now=True)

    class Meta:
        index_together = ['user_id', 'id']
```

## routers.py
WebSocket専用サーバーに登録するURL、マッピングした処理を定義するクラス
- route_name: WebSocket専用サーバーに登録されるURLのPathになる(http://localhost:9999/todo-listな感じ)
- get_initial: クライアントからデータの追加、更新、削除するときに追加で含ませるパラメーターを定義
- get_subscription_contexts: クライアントがsubscriptionしたときのチャネルに含ませるパラメーターを定義
- get_object: データを単体で要求されたときのクエリー処理(必須)
- get_query_set: データをリストで要求されたときのクエリー処理(必須)

```py
# -*- coding: utf-8 -*-

from swampdragon import route_handler
from swampdragon.route_handler import ModelRouter
from module.todo.models import TodoList, TodoItem
from module.todo.serializers import UserSerializer, TodoListSerializer, TodoItemSerializer


class UserRouter(ModelRouter):
    route_name = 'user'
    serializer_class = UserSerializer

    def get_object(self, **kwargs):
        return self.connection.user

    def get_query_set(self, **kwargs):
        pass


class TodoListRouter(ModelRouter):
    route_name = 'todo-list'
    serializer_class = TodoListSerializer
    model = TodoList

    def get_initial(self, verb, **kwargs):
        kwargs['user_id'] = self.connection.user.id
        return kwargs

    def get_subscription_contexts(self, **kwargs):
        # 更新通知がログインユーザーのみに送られるようユーザーIDを入れてユニークなチャネルを作る(todolist|user_id:1なチャネルになる)
        kwargs['user_id'] = self.connection.user.id
        return kwargs

    def get_object(self, **kwargs):
        user_list = self.model.objects.filter(id=kwargs['id'], user_id=self.connection.user.id)
        return user_list[0] if user_list else None

    def get_query_set(self, **kwargs):
        user_id = self.connection.user.id
        return self.model.objects.filter(user_id=user_id)


class TodoItemRouter(ModelRouter):
    route_name = 'todo-item'
    serializer_class = TodoItemSerializer
    model = TodoItem

    def get_initial(self, verb, **kwargs):
        kwargs['user_id'] = self.connection.user.id
        return kwargs

    def get_subscription_contexts(self, **kwargs):
        kwargs['user_id'] = self.connection.user.id
        return kwargs

    def get_object(self, **kwargs):
        user_list = self.model.objects.filter(id=kwargs['id'], user_id=self.connection.user.id)
        return user_list[0] if user_list else None

    def get_query_set(self, **kwargs):
        user_id = self.connection.user.id
        return self.model.objects.filter(user_id=user_id)


route_handler.register(UserRouter)
route_handler.register(TodoListRouter)
route_handler.register(TodoItemRouter)
```

## クライアント
クライアントはAngularJS側を利用する
$dragonがSwampDragonが用意してるサービス。

```js
angular.module('todoApp')
  .controller('PageController', ['$scope', '$dragon', '$dataHandler',
                                 function ($scope, $dragon, $dataHandler) {
    $scope.todoListChannel = 'todoListClient';
    $scope.todoItemChannel = 'todoItemClient';

    // 初回ページアクセス時
    $dragon.onReady(function() {
        // todolist, todoItemの情報を購読する(変更通知されるようなる)
        $dragon.subscribe('todo-list', $scope.todoListChannel, {}).then(function(response) {
            $scope.TodoListMapper = new DataMapper(response.data);
        });

        $dragon.subscribe('todo-item', $scope.todoItemChannel, {}).then(function(response) {
            $scope.todoItemMapper = new DataMapper(response.data);
        });

        // todolist, todoItem, userのデータを取得
        $dragon.getSingle('user', {}).then(function(response) {
            $dataHandler.user = response.data;
        });

        $dragon.getList('todo-list', {list_id: 1}).then(function(response) {
            $dataHandler.todoLists = response.data;
        });

        $dragon.getList('todo-item', {list_id: 1}).then(function(response) {
            $dataHandler.todoItems = response.data;
        });
    });

    // todolist, todoItemでsaveがあった場合の変更通知
    $dragon.onChannelMessage(function(channels, message) {

        if (indexOf.call(channels, $scope.todoListChannel) > -1) {
            $scope.$apply(function() {
                $scope.TodoListMapper.mapData($dataHandler.todoLists, message);
            });
        }

        if (indexOf.call(channels, $scope.todoItemChannel) > -1) {
            $scope.$apply(function() {
                $scope.todoItemMapper.mapData($dataHandler.todoItems, message);
            });
        }
    });

}]);

```


# 起動

```py
python ./manage.py runserver
python server.py
```


# まとめ
サーバーサイドはすごく簡単にでき、Djangoをさわったことがある人には馴染みやすいと思う。
ただ、クライアント側はリアルタイムでHTMLへ画面更新させるJSのコールバックなどの処理が
がんばらないといけなさそうな雰囲気。
AngularJSを利用するとHTMLとJS変数のデータバインディングができるので画面更新の手間は改善できそう。

日本ではあまり導入実績はなさそうだけど、githubを見ると
ちらほら使われているようなので今後に期待したい。

今回作成したTodo管理サイトのコードはgithubに置いてます.
https://github.com/fujimisakari/todo-server