Refactor draft app with improved state management and components

* Rename WebSocket message types for better organization
* Improve state handling with dedicated methods like broadcast_state
* Restructure frontend components and remove unused code
This commit is contained in:
2025-08-24 12:06:41 -05:00
parent b38c779772
commit baddca8d50
22 changed files with 387 additions and 275 deletions

View File

@@ -18,8 +18,8 @@ class DraftMessage(StrEnum):
PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload)
# Status / sync
STATUS_SYNC_REQUEST = "status.sync.request" # client -> server
STATUS_SYNC_INFORM = "status.sync.inform" # server -> client (full/partial state)
DRAFT_STATUS_REQUEST = "draft.status.request" # client -> server
DRAFT_STATUS_INFORM = "draft.status.sync.inform" # server -> client (full/partial state)
DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request"
DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm"

View File

@@ -72,18 +72,12 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
"payload": {"user": self.user.username},
},
)
await self.channel_layer.send(
self.channel_name,
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "direct.message",
"subtype": DraftMessage.STATUS_SYNC_INFORM,
"payload": {
**self.draft_state,
"user": self.user.username,
"participants": [
user.username for user in self.draft_participants
],
},
"subtype": DraftMessage.DRAFT_STATUS_INFORM,
"payload": self.draft_state.to_dict(),
},
)
await self.channel_layer.send(
@@ -101,14 +95,37 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
async def receive_json(self, content):
logger.info(f"receiving message {content}")
event_type = content.get("type")
if event_type == DraftMessage.STATUS_SYNC_REQUEST:
if event_type == DraftMessage.DRAFT_STATUS_REQUEST:
await self.send_json(
{
"type": DraftMessage.STATUS_SYNC_INFORM,
"type": DraftMessage.DRAFT_STATUS_INFORM,
"payload": self.get_draft_status(),
}
)
# --- 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.DRAFT_STATUS_INFORM,
"payload": self.draft_state.to_dict(),
},
)
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.DRAFT_STATUS_INFORM,
"payload": self.draft_state.to_dict(),
},
)
# Broadcast Handlers
async def direct_message(self, event):
await self._dispatch_broadcast(event)
@@ -132,9 +149,15 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
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", "settings",
).prefetch_related("participants").get(pk=draft_session_id)
draft_session = (
DraftSession.objects.select_related(
"season",
"season__league",
"settings",
)
.prefetch_related("participants")
.get(pk=draft_session_id)
)
else:
raise Exception()
@@ -155,89 +178,85 @@ class DraftAdminConsumer(DraftConsumerBase):
await self.channel_layer.group_add(self.group_names.admin, self.channel_name)
def should_accept_user(self):
return super().should_accept_user() and self.user.is_staff
async def receive_json(self, content):
await super().receive_json(content)
logger.info(f"Receive message {content}")
event_type = content.get("type")
if (
event_type == DraftMessage.PHASE_CHANGE_REQUEST
and content.get("destination") == DraftPhase.DETERMINE_ORDER
):
await self.determine_draft_order()
if (
event_type == DraftMessage.PHASE_CHANGE_REQUEST
and content.get("destination") == DraftPhase.NOMINATING
):
await self.start_nominate()
match event_type:
case DraftMessage.PHASE_CHANGE_REQUEST:
destination = content.get('destination')
match destination:
case DraftPhase.DETERMINE_ORDER:
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
self.draft_state.determine_draft_order()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
"payload": {"draft_order": self.draft_state.draft_order},
},
)
await self.broadcast_state()
if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
self.draft_state.draft_index_advance()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
"payload": {**self.draft_state},
},
)
case DraftPhase.NOMINATING:
await self.set_draft_phase(DraftPhase.NOMINATING)
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()
if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST:
movie_id = content.get("payload", {}).get("movie_id")
user = content.get("payload", {}).get("user")
self.draft_state.start_nomination(movie_id)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.NOMINATION_CONFIRM,
"payload": {
"current_movie": self.draft_state[
"current_movie"
],
"nominating_participant": user,
case DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
self.draft_state.draft_index_advance()
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
"payload": {"draft_index": self.draft_state.draft_index},
},
},
)
if event_type == DraftMessage.BID_START_REQUEST:
)
await self.broadcast_state()
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},
},
)
case DraftMessage.NOMINATION_SUBMIT_REQUEST:
movie_id = content.get("payload", {}).get("movie_id")
user = content.get("payload", {}).get("user")
self.draft_state.start_nomination(movie_id)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.NOMINATION_CONFIRM,
"payload": {
"current_movie": self.draft_state["current_movie"],
"nominating_participant": user,
},
},
)
await self.broadcast_state()
def should_accept_user(self):
return super().should_accept_user() and self.user.is_staff
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()
# === Draft logic ===
async def start_nominate(self):
await self.set_draft_phase(DraftPhase.NOMINATING)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
"payload": {"phase": self.draft_state.phase},
},
)
async def determine_draft_order(self):
self.draft_state.determine_draft_order()
next_picks = self.draft_state.next_picks(include_current=True)
await self.channel_layer.group_send(
self.group_names.session,
{
"type": "broadcast.session",
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
"payload": {**self.draft_state},
},
)
# === Draft logic ===
async def set_draft_phase(self, destination: DraftPhase):
self.draft_state.phase = destination
@@ -269,7 +288,9 @@ class DraftParticipantConsumer(DraftConsumerBase):
"subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM,
"payload": {
"user": self.user.username,
"connected_participants": list(self.draft_state.connected_participants),
"connected_participants": list(
self.draft_state.connected_participants
),
},
},
)
@@ -287,7 +308,9 @@ class DraftParticipantConsumer(DraftConsumerBase):
"subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM,
"payload": {
"user": self.user.username,
"connected_participants": list(self.draft_state.connected_participants),
"connected_participants": list(
self.draft_state.connected_participants
),
},
},
)
@@ -315,9 +338,9 @@ class DraftParticipantConsumer(DraftConsumerBase):
},
},
)
if event_type == DraftMessage.BID_PLACE_REQUEST:
bid_amount = content.get('payload',{}).get('bid_amount')
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,

View File

@@ -65,10 +65,11 @@ class DraftCache:
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
class DraftStateManager:
_initial_phase: DraftPhase = DraftPhase.WAITING.value
def __init__(self, session: DraftSession):
self.session_id: str = session.hashid
self.cache: DraftCache = DraftCache(self.session_id, cache)
self._initial_phase: DraftPhase = self.cache.phase or DraftPhase.WAITING.value
self.settings: DraftSessionSettings = session.settings
self.participants: set[User] = set(session.participants.all())
self.connected_participants: set[User] = set()
@@ -76,7 +77,7 @@ class DraftStateManager:
# === Phase Management ===
@property
def phase(self) -> str:
return self.cache.phase
return self.cache.phase or self._initial_phase
@phase.setter
def phase(self, new_phase: DraftPhase) -> None:
@@ -106,7 +107,7 @@ class DraftStateManager:
self.phase = DraftPhase.DETERMINE_ORDER
self.draft_index = 0
draft_order = random.sample(
self.participants, len(self.participants)
list(self.participants), len(self.participants)
)
self.draft_order = [user.username for user in draft_order]
return self.draft_order

View File

@@ -3,6 +3,10 @@
{% load static %}
<script>
window.draftSessionId = "{{ draft_id_hashed }}"
console.log("{{user}}")
</script>
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
{% if user.is_staff %}
<div id="draft-admin-bar-root" data-draft-id="{{ draft_id_hashed }}">You are admin!</div>
{% endif %}
{% endblock body %}

View File

@@ -6,6 +6,7 @@ app_name = "draft"
urlpatterns = [
# path("", views.draft_room, name="room"),
path("session/<str:draft_session_id_hashed>/", views.draft_room, name="session"),
path("session/<str:draft_session_id_hashed>/<str:subpage>", views.draft_room, name="admin_session"),
path("session/<str:draft_session_id_hashed>/debug", views.draft_room_debug, name="session"),
# path("session/<str:draft_session_id_hashed>/<str:subpage>", views.draft_room, name="admin_session"),
# path("<slug:league_slug>/<slug:season_slug>/", views.draft_room_list, name="room"),
]

View File

@@ -6,28 +6,22 @@ from django.contrib.auth.decorators import login_required
from boxofficefantasy_project.utils import decode_id
@login_required(login_url='/login/')
def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None, subpage=""):
def draft_room(request, draft_session_id_hashed=None):
if draft_session_id_hashed:
draft_session_id = decode_id(draft_session_id_hashed)
draft_session = get_object_or_404(DraftSession, id=draft_session_id)
league = draft_session.season.league
season = draft_session.season
elif league_slug and season_slug:
raise NotImplementedError
league = get_object_or_404(League, slug=league_slug)
label, year = parse_season_slug(season_slug)
season = get_object_or_404(Season, league=league, label__iexact=label, year=year)
draft_session = get_object_or_404(DraftSession, season=season)
context = {
"draft_id_hashed": draft_session.hashid,
"league": league,
"season": season,
}
return render(request, "draft/room.dj.html", context)
if subpage == "admin":
return render(request, "draft/room_admin.dj.html", context)
elif subpage == "debug":
return render(request, "draft/room_debug.dj.html", context)
else:
return render(request, "draft/room.dj.html", context)
def draft_room_debug(request, draft_session_id_hashed=None):
if draft_session_id_hashed:
draft_session_id = decode_id(draft_session_id_hashed)
draft_session = get_object_or_404(DraftSession, id=draft_session_id)
return render(request, "draft/room_debug.dj.html", {"draft_id_hashed": draft_session.hashid,})