- Implement WebSocket-based lobby system for quiz participants - Add lobby page with real-time participant updates - Create LobbyConsumer for WebSocket communication - Add routing configuration for WebSocket connections - Update requirements with WebSocket dependencies - Add development and production server documentation This change enables real-time quiz lobbies where participants can join and wait for the quiz to start, with instant updates for all connected users.
118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
import json
|
|
import asyncio
|
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
from channels.db import database_sync_to_async
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
from .models import QuizGame, QuizGameParticipant
|
|
|
|
class LobbyConsumer(AsyncWebsocketConsumer):
|
|
async def connect(self):
|
|
self.join_code = self.scope['url_route']['kwargs']['join_code']
|
|
self.room_group_name = f'game_{self.join_code}'
|
|
self.heartbeat_task = None
|
|
|
|
# Join room group
|
|
await self.channel_layer.group_add(
|
|
self.room_group_name,
|
|
self.channel_name
|
|
)
|
|
await self.accept()
|
|
|
|
# Start heartbeat check
|
|
self.heartbeat_task = asyncio.create_task(self.check_participants_heartbeat())
|
|
|
|
# Send current participants list
|
|
participants = await self.get_participants()
|
|
await self.send(text_data=json.dumps({
|
|
'type': 'participant_list',
|
|
'participants': participants
|
|
}))
|
|
|
|
async def disconnect(self, close_code):
|
|
# Cancel heartbeat task
|
|
if self.heartbeat_task:
|
|
self.heartbeat_task.cancel()
|
|
try:
|
|
await self.heartbeat_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# Leave room group
|
|
await self.channel_layer.group_discard(
|
|
self.room_group_name,
|
|
self.channel_name
|
|
)
|
|
|
|
async def receive(self, text_data):
|
|
text_data_json = json.loads(text_data)
|
|
message_type = text_data_json['type']
|
|
|
|
if message_type == 'heartbeat':
|
|
# Update participant's last activity timestamp
|
|
participant_id = text_data_json.get('participant_id')
|
|
if participant_id:
|
|
await self._update_participant_heartbeat(participant_id)
|
|
|
|
elif message_type == 'update_participants':
|
|
participants = await self.get_participants()
|
|
await self.channel_layer.group_send(
|
|
self.room_group_name,
|
|
{
|
|
'type': 'participant_list_update',
|
|
'participants': participants
|
|
}
|
|
)
|
|
|
|
async def participant_list_update(self, event):
|
|
participants = event['participants']
|
|
await self.send(text_data=json.dumps({
|
|
'type': 'participant_list',
|
|
'participants': participants
|
|
}))
|
|
|
|
@database_sync_to_async
|
|
def get_participants(self):
|
|
game = QuizGame.objects.get(join_code=self.join_code)
|
|
participants = QuizGameParticipant.objects.filter(quiz_game=game)
|
|
return [{'id': str(p.participant_id), 'name': p.display_name} for p in participants]
|
|
|
|
@database_sync_to_async
|
|
def _update_participant_heartbeat(self, participant_id):
|
|
try:
|
|
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
|
|
participant.save() # This will update last_heartbeat due to auto_now=True
|
|
except QuizGameParticipant.DoesNotExist:
|
|
pass
|
|
|
|
@database_sync_to_async
|
|
def _remove_inactive_participants(self):
|
|
# Remove participants who haven't sent a heartbeat in the last 30 seconds
|
|
timeout = timezone.now() - timedelta(seconds=30)
|
|
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
|
QuizGameParticipant.objects.filter(
|
|
quiz_game=quiz_game,
|
|
last_heartbeat__lt=timeout
|
|
).delete()
|
|
|
|
async def check_participants_heartbeat(self):
|
|
while True:
|
|
try:
|
|
await asyncio.sleep(10) # Check every 10 seconds
|
|
await self._remove_inactive_participants()
|
|
|
|
# Send updated participant list
|
|
participants = await self.get_participants()
|
|
await self.channel_layer.group_send(
|
|
self.room_group_name,
|
|
{
|
|
'type': 'participant_list_update',
|
|
'participants': participants
|
|
}
|
|
)
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception as e:
|
|
print(f'Error in heartbeat check: {e}')
|
|
await asyncio.sleep(10) # Wait before retrying
|