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

  • 24
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

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ファイルが複数あったけど割愛してる

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用の設定

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を用意

# -*- 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

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

# -*- 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: クライアントから更新可能なカラムを定義

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でシリアライズ設定したクラスを定義する。

# -*- 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: データをリストで要求されたときのクエリー処理(必須)

# -*- 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が用意してるサービス。

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);
            });
        }
    });

}]);

起動

python ./manage.py runserver
python server.py

まとめ

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

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

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