Integrate draft session support with phase handling and real-time updates

- Added user authentication UI in the base template for navbar.
- Expanded `league.dj.html` to include a new "Draft Sessions" tab showing active drafts.
- Refactored Django views and models to support `DraftSession` with participants and movies.
- Replaced deprecated models like `DraftParticipant` and `DraftMoviePool` with a new schema using `DraftSessionParticipant`.
- Introduced WebSocket consumers (`DraftAdminConsumer`, `DraftParticipantConsumer`) with structured phase logic and caching.
- Added `DraftStateManager` for managing draft state in Django cache.
- Created frontend UI components in React for draft admin and participants, including phase control and WebSocket message logging.
- Updated SCSS styles for improved UI structure and messaging area.
This commit is contained in:
2025-08-02 08:56:41 -05:00
parent 1a7a6a2d50
commit c9ce7a36d0
16 changed files with 811 additions and 484 deletions

View File

@@ -3,61 +3,125 @@ from channels.db import database_sync_to_async
from django.core.exceptions import PermissionDenied
from boxofficefantasy.models import League, Season
from boxofficefantasy.views import parse_season_slug
from draft.models import DraftSession, DraftPick, DraftMoviePool, DraftParticipant
from draft.models import DraftSession, DraftSessionParticipant
from django.core.cache import cache
from draft.constants import DraftMessage, DraftPhase, DraftGroupChannelNames
import asyncio
from django.contrib.auth.models import User
from draft.constants import (
DraftMessage,
DraftPhase,
DraftGroupChannelNames,
)
from draft.state import DraftCacheKeys, DraftStateManager
import random
class DraftConsumerBase(AsyncJsonWebsocketConsumer):
group_names: DraftGroupChannelNames
cache_keys: DraftCacheKeys
draft_state: DraftStateManager
user: User
async def connect(self):
draft_session_id_hashed = self.scope["url_route"]["kwargs"].get(
"draft_session_id_hashed"
)
league_slug = self.scope["url_route"]["kwargs"].get("league_slug")
season_slug = self.scope["url_route"]["kwargs"].get("season_slug")
draft_hashid = self.scope["url_route"]["kwargs"].get("draft_session_id_hashed")
self.draft_session = await self.get_draft_session(
draft_session_id_hashed=draft_session_id_hashed,
draft_session_id_hashed=draft_hashid,
)
self.draft_participants = await self.get_draft_participants(
session=self.draft_session
)
self.draft_group_names = f"draft_admin_{self.draft_session.hashed_id}"
self.draft_participant_group_channels = DraftGroupChannelNames(draft_session_id_hashed)
self.group_names = DraftGroupChannelNames(draft_hashid)
self.cache_keys = DraftCacheKeys(draft_hashid)
self.draft_state = DraftStateManager(draft_hashid)
self.user = self.scope["user"]
if not self.user.is_authenticated:
if not self.should_accept_user():
await self.send_json({
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
"user": self.user.username
})
await self.close()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
"user": self.user.username
},
)
return
else:
await self.accept()
await self.channel_layer.group_add(
self.group_names.session, self.channel_name
)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": DraftMessage.INFORM_JOIN_USER,
"user": self.user.username
},
)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": DraftMessage.INFORM_PHASE,
"phase": str(self.draft_state.phase)
}
)
async def should_accept_user(self)->bool:
return self.user.is_authenticated
async def receive_json(self, content):
event_type = content.get("type")
user = self.scope["user"]
async def user_joined(self, event):
async def inform_leave_participant(self,event):
await self.send_json(
{
"type": "user.joined",
"user": event["user"].username,
"user_type": event["user_type"],
"users": event["users"],
"type": event["type"],
"user": event["user"],
"participants": [user.username for user in self.draft_participants],
"connected_participants": self.draft_state.connected_users
}
)
async def send_draft_summary(self):
state = cache.get(self.draft_status_cache_key, {})
async def inform_join_user(self, event):
await self.send_json(
{
"type": "draft_summary",
"phase": state.get("phase", "not started"),
"movie": state.get("movie"),
"current_bid": state.get("current_bid"),
"time_remaining": state.get("time_remaining"),
"you_are_next": state.get("you_are_next", False),
"type": event["type"],
"user": event["user"],
"participants": [user.username for user in self.draft_participants],
"connected_participants": self.draft_state.connected_users
}
)
async def reject_join_participant(self,event):
await self.send_json(
{
"type": event["type"],
"user": event["user"],
"participants": [user.username for user in self.draft_participants],
"connected_participants": self.draft_state.connected_users
}
)
async def inform_phase(self, event):
await self.send_json(
{
"type": event['type'],
"phase": event['phase']
}
)
async def confirm_determine_draft_order(self, event):
await self.send_json(
{"type": DraftMessage.CONFIRM_DETERMINE_DRAFT_ORDER, "payload": event["payload"]}
)
async def send_draft_summary(self): ...
# === Broadcast handlers ===
async def draft_status(self, event):
await self.send_json(
@@ -69,36 +133,20 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
# === DB Access ===
@database_sync_to_async
def get_draft_session(
self, draft_session_id_hashed, league_slug, season_slug
) -> DraftSession:
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
draft_session_id = DraftSession.decode_id(draft_session_id_hashed)
if draft_session_id:
draft_session = DraftSession.objects.select_related(
"season", "season__league"
).get(pk=draft_session_id)
elif league_slug and season_slug:
label, year = parse_season_slug(season_slug)
season = Season.objects.filter(label=label, year=year).first()
draft_session = (
DraftSession.objects.select_related("season", "season__league")
.filter(season=season)
.first()
)
else:
raise Exception()
return draft_session
@database_sync_to_async
def get_draft_participants(self) -> list[DraftParticipant]:
# Replace this with real queryset to fetch users in draft
participants = DraftParticipant.objects.select_related("user").filter(
draft=self.draft_session
)
connected_ids = cache.get(self.draft_connected_participants_cache_key, set())
for p in participants:
p.is_connected = p in connected_ids
def get_draft_participants(self, session) -> list[DraftSessionParticipant]:
participants = session.participants.all()
return list(participants.all())
@@ -109,192 +157,118 @@ class DraftAdminConsumer(DraftConsumerBase):
await self.close()
return
await self.channel_layer.group_add(
self.draft_admin_group_name, self.channel_name
await self.channel_layer.group_add(self.group_names.admin, self.channel_name)
async def receive_json(self, content):
await super().receive_json(content)
event_type = content.get("type")
user = self.scope["user"]
destination = DraftPhase(content.get("destination"))
if (
event_type == DraftMessage.REQUEST_PHASE_CHANGE
and destination == DraftPhase.DETERMINE_ORDER
):
await self.determine_draft_order()
def should_accept_user(self):
return super().should_accept_user() and self.user.is_staff
# === Draft logic ===
async def determine_draft_order(self):
draft_order = random.sample(self.draft_participants, len(self.draft_participants))
self.draft_state.draft_order = [p.username for p in draft_order]
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": DraftMessage.CONFIRM_DETERMINE_DRAFT_ORDER,
"payload": {
"draft_order": self.draft_state.draft_order
},
},
)
# await self.channel_layer.group_send(
# self.draft_participant_group_name,
# {"type": "user.joined", "user": self.user, "user_type": "admin"},
# )
async def set_draft_phase(self, destination: DraftPhase):
self.draft_state.phase = destination
await self.channel_layer.group_send(
self.draft_admin_group_name,
{"type": "user.joined", "user": self.user, "user_type": "admin"},
self.group_names.session,
{
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
"payload": {
"phase": self.draft_state.phase
},
},
)
# === Broadcast Handlers ===
async def confirm_phase_change(self, event):
await self.send_json({
"type": event["type"],
"payload": event["payload"]
})
class DraftParticipantConsumer(DraftConsumerBase):
async def connect(self):
await super().connect()
self.draft_state.connect_user(self.user.username)
await self.channel_layer.group_add(
self.group_names.participant, self.channel_name
)
async def disconnect(self, close_code):
await self.channel_layer.group_send(
self.group_names.session,
{
"type": DraftMessage.INFORM_LEAVE_PARTICIPANT,
"user": self.user.username
},
)
await super().disconnect(close_code)
self.draft_state.disconnect_user(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
async def receive_json(self, content):
await super().receive_json(content)
event_type = content.get("type")
user = self.scope["user"]
if event_type == "start.draft":
await self.start_draft()
elif event_type == "user.joined":
pass
elif event_type == "nominate":
await self.nominate(content.get("movie"))
elif event_type == "bid":
await self.place_bid(content.get("amount"), self.scope["user"].username)
elif event_type == "message":
if event_type == DraftMessage.REQUEST_JOIN_PARTICIPANT:
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "chat.message",
"user": self.scope["user"].username,
"message": content.get("message"),
},
)
# === Draft logic (stubbed for now) ===
async def start_draft(self):
# Example: shuffle draft order
participants = await self.get_draft_participants()
draft_order = random.sample(participants, len(participants))
connected_participants = cache.get(
self.draft_connected_participants_cache_key, ()
)
initial_state = {
"phase": "nominating",
"current_nominee": None,
"current_bid": None,
"participants": [
{"user": p.user.username, "is_connected": p in connected_participants}
for p in await self.get_draft_participants()
],
"draft_order": [p.user.username for p in draft_order],
"current_turn_index": 0,
"picks": [],
}
cache.set(self.draft_status_cache_key, initial_state)
for group_name in [
self.draft_admin_group_name,
self.draft_participant_group_name,
]:
# await self.channel_layer.group_send(
# group_name,
# {
# "type": "draft.start"
# }
# )
await self.channel_layer.group_send(
group_name,
{
"type": "draft.status",
"status": cache.get(self.draft_status_cache_key),
},
)
class DraftParticipantConsumer(DraftConsumerBase):
async def connect(self):
await super().connect()
await self.channel_layer.group_add(
self.draft_participant_group_name, self.channel_name
)
try:
await self.add_draft_participant()
except Exception as e:
await self.close()
return
await self.send_json(
{
"type": "connection.accepted",
"user": self.user.username,
"is_staff": self.user.is_staff,
}
)
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "user.joined",
"user": self.user,
"user_type": "participant",
"participants": [],
},
)
await self.channel_layer.group_send(
self.draft_admin_group_name,
{"type": "user.joined", "user": self.user, "user_type": "participant"},
)
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.draft_participant_group_name, self.channel_name
)
async def receive_json(self, content):
event_type = content.get("type")
user = self.scope["user"]
if event_type == "user.joined":
pass
elif event_type == "nominate":
await self.nominate(content.get("movie"))
elif event_type == "bid":
await self.place_bid(content.get("amount"), self.scope["user"].username)
elif event_type == "message":
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "chat.message",
"user": self.scope["user"].username,
"message": content.get("message"),
},
self.group_names.admin,
{"type": DraftMessage.REQUEST_JOIN_PARTICIPANT, "user": user},
)
# === Broadcast handlers ===
async def chat_message(self, event):
async def request_join_participant(self, event):
await self.send_json(
{
"type": "chat.message",
"type": event["type"],
"user": event["user"],
}
)
async def draft_update(self, event):
await self.send_json(
{
"type": "draft.update",
"state": event["state"],
}
)
# === Draft ===
# === Draft logic (stubbed for now) ===
async def nominate(self, movie_title): ...
async def nominate(self, movie_title):
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "draft.update",
"state": {
"status": "nominating",
"movie": movie_title,
},
},
)
async def place_bid(self, amount, user):
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "draft.update",
"state": {"status": "bidding", "bid": {"amount": amount, "user": user}},
},
)
async def place_bid(self, amount, user): ...
# === Example DB Access ===
@database_sync_to_async
def add_draft_participant(self):
self.participant, _ = DraftParticipant.objects.get_or_create(
self.participant, _ = DraftSessionParticipant.objects.get_or_create(
user=self.user,
draft=self.draft_session,
defaults={"budget": self.draft_session.settings.starting_budget},