LoginSignup
23
25

More than 5 years have passed since last update.

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

Posted at

はじめに

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

23
25
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
23
25