Django Channlesで投票アプリ(簡易版)を作る
先日やったDjango Channels
のチュートリアルを応用して投票アプリを作成してみる。
WebSocketの練習にもぴったりですね。
前提
- Django: 2.2.8
- Channels: 2.4.0
手順
投票用アプリの作成
次のコマンドを叩いて投票用アプリを作成する
$ python manage startapp vote
テンプレートを作成する
問題作成用HTML, 回答用HTMLを作る
$ mkdir <project-root>/vote/templates/question $ touch <project-root>/vote/templates/question/question.html $ mkdir <project-root>/vote/templates/answer $ touch <project-root>/vote/templates/answer/answer.html
<!-- question.html --> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Create Question</title> <style> lavel { width: 100%; display: flex; } </style> </head> <body> <h1>Create Question [ Room Name: {{ room_name }} ]</h1> <label> <span>Question: </span> <input type="text" id="vote-question-input" size="100"> </label> <input type="button" id="vote-question-send" value="Send"> <div> 回答状況: <span id="vote-results"></span> </div> <script> const roomName = '{{ room_name }}' // WebSocketの定義 const questionSocket = new WebSocket('ws://' + window.location.host + '/ws/vote/' + roomName + '/') let answers = [] // サーバからのメッセージ受信時処理 questionSocket.onmessage = (e) => { const data = JSON.parse(e.data) if(data.type !== 'send_answer') return; // let resultsData = document.getElementById('vote-resutls').innerHTML || '' // resultsData += data.answer + ',' // document.getElementById('vote-results').innerHTML = resultsData answers.push(data.answer) document.getElementById('vote-results').innerHTML = answers.join(',') } // WebSocket終了時処理 questionSocket.onclose = (e) => { console.error('Question socket closed.') } // 問題送信処理 document.getElementById('vote-question-send').onclick = (e) => { const questionInputDom = document.getElementById('vote-question-input') const question = questionInputDom.value; if (question === '') return; let questionResult = {} questionResult.question = question questionSocket.send(JSON.stringify({ 'question': question })) questionInputDom.value = '' } </script> </body> </html> <!-- answer.html --> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vote Answer</title> <style> lavel { width: 100%; display: flex; } #vote-question-answers { display: none; } #vote-question-answers.show { display: block; } </style> </head> <body> <h1>Create Question [ Room Name: {{ room_name }} ]</h1> <label> <span>Question: </span> <div id="vote-question-text">投票前です</div> </label> <div id="vote-question-answers"> <!-- 今回はデモなので選択肢の文言を固定とする --> <label><input type="radio" name="vote-answer" value="大分県"> 大分県</label> <label><input type="radio" name="vote-answer" value="大分県以外"> 大分県以外</label> <input type="button" id="vote-question-send" value="Send"> </div> <script> const roomName = '{{ room_name }}' // WebSocketの定義 const answerSocket = new WebSocket('ws://' + window.location.host + '/ws/vote/' + roomName + '/') // サーバからのメッセージ受信時処理 answerSocket.onmessage = (e) => { const data = JSON.parse(e.data); if(data.type !== 'send_question') return; document.getElementById('vote-question-text').innerHTML = data.question document.getElementById('vote-question-answers').classList.toggle('show') } // WebSocket終了時処理 answerSocket.onclose = (e) => { console.error('Question socket closed.') } // 問題送信処理 document.getElementById('vote-question-send').onclick = (e) => { const answerInputDoms = document.querySelectorAll('[name=vote-answer]') const answers = {} for(let i = 0; i < answerInputDoms.length; i++) { if(!answerInputDoms[i].checked) continue answers.answer = answerInputDoms[i].value } if (Object.keys(answers).length == 0) return; answerSocket.send(JSON.stringify(answers)) document.getElementById('vote-question-text').innerHTML = '回答しました' document.getElementById('vote-question-answers').classList.toggle('show') } </script> </body> </html>
Views, Rouinの設定を行う
question.html, answer.htmlを利用できるよう views
の設定を行う
# <project-root>/vote/views.py from django.shortcuts import render def index(request): pass def question(request, room_name): return render(request, 'question/question.html', { 'room_name': room_name }) def answer(request, room_name): return render(request, 'answer/answer.html', { 'room_name': room_name })
viewに対する routing
の設定を行う
# <project-root>/<project-app>/urls.py urlpatterns = [ ... path('vote/', include('vote.urls')), ] # <project-root>/vote/urls.py from django.urls import path from . import views urlpatterns = [ path('question/<str:room_name>/', views.question, name='question'), path('answer/<str:room_name>/', views.answer, name='answer'), ]
Websocketの処理を作成する
WebSocketを利用するために consumer
を作成する
複数のグループに属する場合のうまい書き方がわからなくてself.channel_layer.group_add()
を複数回書いてるけどこんなんでいいのかな…
import json from channels.generic.websocket import AsyncWebsocketConsumer class VoteConsumer(AsyncWebsocketConsumer): # 接続時処理 async def connect(self): # チャンネル名とグループ名を定義 self.room_group_name_question = 'question_%s' % self.scope['url_route']['kwargs']['room_name'] self.room_group_name_answer = 'answer_%s' % self.scope['url_route']['kwargs']['room_name'] # グループに追加 await self.channel_layer.group_add( self.room_group_name_question, self.channel_name ) await self.channel_layer.group_add( self.room_group_name_answer, self.channel_name ) await self.accept() # 切断時処理 async def disconnect(self, close_code): # グループから削除 await self.channel_layer.group_discard( self.room_group_name_question, self.channel_name ) await self.channel_layer.group_discard( self.room_group_name_answer, self.channel_name ) await self.close() # メッセージ受信時処理 async def receive(self, text_data): text_data_json = json.loads(text_data) if 'question' in text_data_json.keys(): await self.channel_layer.group_send( self.room_group_name_answer, { 'type': 'send_question', 'question': text_data_json['question'] } ) if 'answer' in text_data_json.keys(): await self.channel_layer.group_send( self.room_group_name_question, { 'type': 'send_answer', 'answer': text_data_json['answer'] } ) # 質問の送信 async def send_question(self, event): question = event['question'] await self.send(text_data=json.dumps({ 'type': 'send_question', 'question': question })) # 回答の送信 async def send_answer(self, event): answer = event['answer'] await self.send(text_data=json.dumps({ 'type': 'send_answer', 'answer': answer }))
WebSocket用のルーティングを設定する
前述のConsumer
に対する routing
を設定する
これを設定することでWebSocketのルーティングが有効化される
# <project-root>/<project-app>/asgi.py ... import django.urls import re_path import vote.consumers application = ProtocolTypeRouter({ "http": AsgiHandler, "websocket": AuthMiddlewareStack( URLRouter([ ..., re_path(r'ws/vote/(?P<room_name>\w+)/$', vote.consumers.VoteConsumer), ]) ), })
動作確認
ここまでできたらサーバを起動する(Channels用のRedisも起動しておく)
$ redis-server & $ python manage.py runserver 0.0.0.0:8000
ブラウザでhttp://localhost:8000/vote/question/lobby
, http://localhost:8000/vote/answer/lobby
にアクセスしてみる。
/vote/question/lobby
で質問を入力し送信すると、/vote/answer/lobby
側で質問が反映される。質問に回答すると/vote/question/lobby
側に回答が追加されるのがわかる。
今回は回答を固定したり、回答者の情報は送信しないなど本当に必要最低限の機能しか実装していないが、あとは作り込みの問題なので割愛とする
Django ChannelsがあるとWebSocketの利用が楽になっていいですね。