Add user state updates and bidding error handling in draft consumers

- Implement user state tracking and broadcasting on connect/disconnect and phase changes
- Add bid start and place rejection handling with error messages to frontend and backend
- Enhance movie serializer with TMDB integration and update relevant frontend components
This commit is contained in:
2025-08-24 17:16:22 -05:00
parent baddca8d50
commit 5e08fdc9a2
15 changed files with 385 additions and 242 deletions

View File

@@ -4,23 +4,19 @@ from django.core.exceptions import PermissionDenied
from boxofficefantasy.models import League, Season
from boxofficefantasy.views import parse_season_slug
from draft.models import DraftSession, DraftSessionParticipant
import asyncio
from django.contrib.auth.models import User
from draft.constants import (
DraftMessage,
DraftPhase,
DraftGroupChannelNames,
)
from draft.state import DraftStateManager
from draft.state import DraftStateManager, DraftStateException
from typing import Any
import logging
logger = logging.getLogger(__name__) # __name__ = module path
import random
class DraftConsumerBase(AsyncJsonWebsocketConsumer):
group_names: DraftGroupChannelNames
draft_state: DraftStateManager
@@ -61,6 +57,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
return
else:
await self.accept()
self.draft_state.connect_participant(self.user.username)
await self.channel_layer.group_add(
self.group_names.session, self.channel_name
)
@@ -72,14 +69,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
"payload": {"user": self.user.username},
},
)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "direct.message",
"subtype": DraftMessage.DRAFT_STATUS_INFORM,
"payload": self.draft_state.to_dict(),
},
)
await self.channel_layer.send(
self.channel_name,
{
@@ -88,6 +77,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
"payload": {"user": self.user.username},
},
)
await self.broadcast_state()
async def should_accept_user(self) -> bool:
return self.user.is_authenticated
@@ -106,6 +96,14 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
# --- Convenience helpers ---
async def send_draft_state(self):
"""Send the current draft state only to this client."""
await self.channel_layer.send(
self.channel_name,
{
"type": "direct.message",
"subtype": DraftMessage.USER_STATE_INFORM,
"payload": self.draft_state.user_state(self.user),
}
)
await self.channel_layer.send(
self.channel_name,
{
@@ -117,6 +115,14 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
async def broadcast_state(self):
"""Broadcast current draft state to all in session group."""
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.USER_STATE_INFORM,
"payload": [self.draft_state.user_state(user) for user in self.draft_participants],
}
)
await self.channel_layer.group_send(
self.group_names.session,
{
@@ -214,6 +220,18 @@ class DraftAdminConsumer(DraftConsumerBase):
},
)
await self.broadcast_state()
case DraftPhase.BIDDING:
await self.set_draft_phase(DraftPhase.BIDDING)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
"payload": {"phase": self.draft_state.phase},
},
)
await self.broadcast_state()
case DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
self.draft_state.draft_index_advance()
@@ -245,16 +263,26 @@ class DraftAdminConsumer(DraftConsumerBase):
await self.broadcast_state()
case DraftMessage.BID_START_REQUEST:
self.draft_state.start_bidding()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.BID_START_INFORM,
"payload": {**self.draft_state},
},
)
await self.broadcast_state()
try:
self.draft_state.start_bidding()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.BID_START_INFORM,
"payload": {**self.draft_state},
},
)
await self.broadcast_state()
except DraftStateException as e:
await self.channel_layer.send(
self.channel_name, {
"type": "direct.message",
"subtype": DraftMessage.BID_START_REJECT,
"payload": {'message': str(e)}
}
)
# === Draft logic ===
@@ -314,12 +342,14 @@ class DraftParticipantConsumer(DraftConsumerBase):
},
},
)
await self.broadcast_state()
await super().disconnect(close_code)
self.draft_state.disconnect_participant(self.user.username)
await self.channel_layer.group_discard(
self.group_names.session, self.channel_name
)
def should_accept_user(self):
return super().should_accept_user() and self.user in self.draft_participants
@@ -333,7 +363,7 @@ class DraftParticipantConsumer(DraftConsumerBase):
"type": "broadcast.admin",
"subtype": event_type,
"payload": {
"movie_id": content.get("payload", {}).get("id"),
"movie_id": content.get("payload", {}).get("movie_id"),
"user": content.get("payload", {}).get("user"),
},
},
@@ -341,15 +371,26 @@ class DraftParticipantConsumer(DraftConsumerBase):
if event_type == DraftMessage.BID_PLACE_REQUEST:
bid_amount = content.get("payload", {}).get("bid_amount")
self.draft_state.place_bid(self.user, bid_amount)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.BID_PLACE_CONFIRM,
"payload": {**self.draft_state},
},
)
try:
self.draft_state.place_bid(self.user, bid_amount)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.BID_PLACE_CONFIRM,
"payload": {'user': self.user.username, 'bid': bid_amount},
},
)
except DraftStateException as e:
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.BID_PLACE_REJECT,
"payload": {'user': self.user.username, 'bid': bid_amount, 'error':str(e)},
},
)
await self.broadcast_state()
# === Broadcast handlers ===
@@ -358,11 +399,8 @@ class DraftParticipantConsumer(DraftConsumerBase):
# === Draft ===
async def nominate(self, movie_title): ...
async def place_bid(self, amount, user): ...
# === Example DB Access ===
# === DB Access ===
@database_sync_to_async
def add_draft_participant(self):