Refactor draft messaging to unified enum-based protocol
- Replaced scattered message strings with `DraftMessage` `StrEnum` and numeric `DraftPhase` `IntEnum` for clear, centralized definitions. - Added Python→JS constants sync via `scripts/generate_js_constants.py` to ensure backend/frontend parity. - Refactored WebSocket consumers to use `broadcast.*` and `direct.message` handlers with `_dispatch_broadcast` for consistent event delivery. - Enhanced `DraftStateManager` to store `draft_index` and explicitly manage `connected_participants`. - Added colored logging config in settings for improved debugging. - Frontend: split UI into `ParticipantList` and `DraftMoviePool`, extracted message handlers (`handleDraftStatusMessages`, `handleUserIdentifyMessages`), and updated components to use new message/phase enums.
This commit is contained in:
@@ -151,3 +151,28 @@ CHANNEL_LAYERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HASHIDS_SALT = os.getenv("BOF_HASHIDS_SALT", "your-very-secret-salt-string")
|
HASHIDS_SALT = os.getenv("BOF_HASHIDS_SALT", "your-very-secret-salt-string")
|
||||||
|
|
||||||
|
COLOR_GREEN = "\033[92m"
|
||||||
|
COLOR_RESET = "\033[0m"
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'prefix': {
|
||||||
|
'format': f'{COLOR_GREEN}[%(name)s]{COLOR_RESET} %(levelname)s %(asctime)s %(name)s: %(message)s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'prefix'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'draft.consumers': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO', # Only INFO and above
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,51 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum, StrEnum
|
||||||
|
|
||||||
class DraftMessage:
|
class DraftMessage(StrEnum):
|
||||||
# Server
|
# Participant
|
||||||
INFORM_PHASE_CHANGE = "inform.phase.change"
|
PARTICIPANT_JOIN_REQUEST = "participant.join.request" # client -> server
|
||||||
CONFIRM_PHASE_CHANGE = "confirm.phase.change"
|
PARTICIPANT_JOIN_CONFIRM = "participant.join.confirm" # server -> client
|
||||||
INFORM_PHASE = "inform.phase"
|
PARTICIPANT_JOIN_REJECT = "participant.join.reject" # server -> client
|
||||||
INFORM_DRAFT_STATUS = "inform.draft_status"
|
PARTICIPANT_LEAVE_INFORM = "participant.leave.inform" # server -> client (broadcast)
|
||||||
|
|
||||||
# Client
|
# User presence
|
||||||
REQUEST_PHASE_CHANGE = "request.phase.change"
|
USER_JOIN_INFORM = "user.join.inform" # server -> client
|
||||||
REQUEST_DRAFT_STATUS = "request.draft_status"
|
USER_LEAVE_INFORM = "user.leave.inform"
|
||||||
|
USER_IDENTIFICATION_INFORM = "user.identification.inform" # server -> client (tells socket "you are X", e.g. after connect) # server -> client
|
||||||
|
|
||||||
# Waiting Phase
|
# Phase control
|
||||||
## Server
|
PHASE_CHANGE_INFORM = "phase.change.inform" # server -> client (target phase payload)
|
||||||
INFORM_JOIN_USER = "inform.join.user"
|
PHASE_CHANGE_REQUEST = "phase.change.request" # server -> client (target phase payload)
|
||||||
REQUEST_JOIN_PARTICIPANT = "request.join.participant"
|
PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload)
|
||||||
REQUEST_JOIN_ADMIN = "request.join.admin"
|
|
||||||
INFORM_LEAVE_PARTICIPANT = "inform.leave.participant"
|
|
||||||
|
|
||||||
## Client
|
# Status / sync
|
||||||
NOTIFY_JOIN_USER = "notify.join.user"
|
STATUS_SYNC_REQUEST = "status.sync.request" # client -> server
|
||||||
CONFIRM_JOIN_PARTICIPANT = "confirm.join.participant"
|
STATUS_SYNC_INFORM = "status.sync.inform" # server -> client (full/partial state)
|
||||||
REJECT_JOIN_PARTICIPANT = "reject.join.participant"
|
|
||||||
CONFIRM_JOIN_ADMIN = "confirm.join.admin"
|
|
||||||
|
|
||||||
# Determine Order
|
DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request"
|
||||||
## Server
|
DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm"
|
||||||
CONFIRM_DETERMINE_DRAFT_ORDER = "confirm.determine.draft_order"
|
|
||||||
## Client
|
# Order determination
|
||||||
REQUEST_DETERMINE_DRAFT_ORDER = "request.determine.draft_order"
|
ORDER_DETERMINE_REQUEST = "order.determine.request" # client -> server (admin)
|
||||||
|
ORDER_DETERMINE_CONFIRM = "order.determine.confirm" # server -> client
|
||||||
|
|
||||||
|
# Bidding (examples, adjust to your flow)
|
||||||
|
BID_START_INFORM = "bid.start.inform" # server -> client (movie, ends_at)
|
||||||
|
BID_PLACE_REQUEST = "bid.place.request" # client -> server (amount)
|
||||||
|
BID_UPDATE_INFORM = "bid.update.inform" # server -> client (high bid)
|
||||||
|
BID_END_INFORM = "bid.end.inform" # server -> client (winner)
|
||||||
|
|
||||||
|
# Nomination (examples)
|
||||||
|
NOMINATION_SUBMIT_REQUEST = "nomination.submit.request" # client -> server (movie_id)
|
||||||
|
NOMINATION_CONFIRM = "nomination.submit.confirm" # server -> client
|
||||||
|
|
||||||
|
|
||||||
class DraftPhase(IntEnum):
|
class DraftPhase(IntEnum):
|
||||||
WAITING = 0
|
WAITING = 10
|
||||||
DETERMINE_ORDER = 10
|
DETERMINE_ORDER = 20
|
||||||
NOMINATION = 20
|
NOMINATING = 30
|
||||||
BIDDING = 30
|
BIDDING = 40
|
||||||
AWARD = 40
|
AWARDING = 50
|
||||||
FINALIZE = 50
|
FINALIZING = 60
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name.lower()
|
return self.name.lower()
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ from draft.constants import (
|
|||||||
)
|
)
|
||||||
from draft.state import DraftCacheKeys, DraftStateManager
|
from draft.state import DraftCacheKeys, DraftStateManager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) # __name__ = module path
|
||||||
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -40,18 +44,21 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
self.user = self.scope["user"]
|
self.user = self.scope["user"]
|
||||||
if not self.should_accept_user():
|
if not self.should_accept_user():
|
||||||
await self.send_json(
|
await self.channel_layer.send(
|
||||||
|
self.channel_name,
|
||||||
{
|
{
|
||||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
"type": "direct.message",
|
||||||
"user": self.user.username,
|
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
|
||||||
|
"payload":{"current_user": self.user.username}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await self.close()
|
await self.close()
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.admin,
|
||||||
{
|
{
|
||||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
"type": "broadcast.admin",
|
||||||
"user": self.user.username,
|
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
|
||||||
|
"payload":{"user": self.user.username}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -62,81 +69,56 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
)
|
)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{"type": DraftMessage.INFORM_JOIN_USER, "user": self.user.username},
|
|
||||||
)
|
|
||||||
await self.send_json(
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": DraftMessage.INFORM_DRAFT_STATUS,
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.USER_JOIN_INFORM,
|
||||||
|
"payload": {"user": self.user.username},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.channel_layer.send(
|
||||||
|
self.channel_name,
|
||||||
|
{
|
||||||
|
"type": "direct.message",
|
||||||
|
"subtype": DraftMessage.STATUS_SYNC_INFORM,
|
||||||
"payload": self.get_draft_status(),
|
"payload": self.get_draft_status(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await self.channel_layer.send(
|
||||||
|
self.channel_name,
|
||||||
|
{
|
||||||
|
"type": "direct.message",
|
||||||
|
"subtype": DraftMessage.USER_IDENTIFICATION_INFORM,
|
||||||
|
"payload": {"user": self.user.username},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def should_accept_user(self) -> bool:
|
async def should_accept_user(self) -> bool:
|
||||||
return self.user.is_authenticated
|
return self.user.is_authenticated
|
||||||
|
|
||||||
async def receive_json(self, content):
|
async def receive_json(self, content):
|
||||||
event_type = content.get("type")
|
event_type = content.get("type")
|
||||||
if event_type == DraftMessage.REQUEST_DRAFT_STATUS:
|
if event_type == DraftMessage.STATUS_SYNC_REQUEST:
|
||||||
await self.send_json(
|
await self.send_json(
|
||||||
{
|
{
|
||||||
"type": DraftMessage.INFORM_DRAFT_STATUS,
|
"type": DraftMessage.STATUS_SYNC_INFORM,
|
||||||
"payload": self.get_draft_status(),
|
"payload": self.get_draft_status(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def inform_leave_participant(self, event):
|
# Broadcast Handlers
|
||||||
await self.send_json(
|
async def direct_message(self, event):
|
||||||
{
|
await self._dispatch_broadcast(event)
|
||||||
"type": event["type"],
|
|
||||||
"user": event["user"],
|
|
||||||
"payload": {
|
|
||||||
"participants": [user.username for user in self.draft_participants],
|
|
||||||
"connected_participants": self.draft_state.connected_users,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inform_join_user(self, event):
|
async def broadcast_session(self, event):
|
||||||
await self.send_json(
|
await self._dispatch_broadcast(event)
|
||||||
{
|
|
||||||
"type": event["type"],
|
|
||||||
"payload": {
|
|
||||||
"user": event["user"],
|
|
||||||
"participants": [user.username for user in self.draft_participants],
|
|
||||||
"connected_participants": self.draft_state.connected_users,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inform_draft_status(self, event):
|
async def _dispatch_broadcast(self, event):
|
||||||
await self.send_json(
|
logger.info(f"dispatching message {event}")
|
||||||
{"type": event["type"], "payload": self.get_draft_status()}
|
subtype = event.get("subtype")
|
||||||
)
|
payload = event.get("payload", {})
|
||||||
|
await self.send_json({"type": subtype, "payload": payload})
|
||||||
|
|
||||||
async def reject_join_participant(self, event):
|
# === Methods ===
|
||||||
await self.send_json(
|
|
||||||
{
|
|
||||||
"type": event["type"],
|
|
||||||
"user": event["user"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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 confirm_phase_change(self, event):
|
|
||||||
await self.send_json({"type": event["type"], "payload": event["payload"]})
|
|
||||||
|
|
||||||
async def send_draft_summary(self): ...
|
|
||||||
|
|
||||||
def get_draft_status(self) -> dict[str, Any]:
|
def get_draft_status(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -145,8 +127,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
"participants": [user.username for user in self.draft_participants],
|
"participants": [user.username for user in self.draft_participants],
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Broadcast handlers ===
|
|
||||||
|
|
||||||
# === DB Access ===
|
# === DB Access ===
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
||||||
@@ -177,29 +157,42 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
|
|
||||||
async def receive_json(self, content):
|
async def receive_json(self, content):
|
||||||
await super().receive_json(content)
|
await super().receive_json(content)
|
||||||
|
logger.info(f"Receive message {content}")
|
||||||
event_type = content.get("type")
|
event_type = content.get("type")
|
||||||
if (
|
if (
|
||||||
event_type == DraftMessage.REQUEST_PHASE_CHANGE
|
event_type == DraftMessage.PHASE_CHANGE_REQUEST
|
||||||
and content.get("destination") == DraftPhase.DETERMINE_ORDER
|
and content.get("destination") == DraftPhase.DETERMINE_ORDER
|
||||||
):
|
):
|
||||||
await self.determine_draft_order()
|
await self.determine_draft_order()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event_type == DraftMessage.REQUEST_PHASE_CHANGE
|
event_type == DraftMessage.PHASE_CHANGE_REQUEST
|
||||||
and content.get("destination") == DraftPhase.NOMINATION
|
and content.get("destination") == DraftPhase.NOMINATING
|
||||||
):
|
):
|
||||||
await self.start_nominate();
|
await self.start_nominate()
|
||||||
|
|
||||||
|
if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
|
||||||
|
self.draft_state.draft_index += 1
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
self.group_names.session,
|
||||||
|
{
|
||||||
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
|
||||||
|
"payload": self.draft_state.get_summary(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def should_accept_user(self):
|
def should_accept_user(self):
|
||||||
return super().should_accept_user() and self.user.is_staff
|
return super().should_accept_user() and self.user.is_staff
|
||||||
|
|
||||||
# === Draft logic ===
|
# === Draft logic ===
|
||||||
async def start_nominate(self):
|
async def start_nominate(self):
|
||||||
await self.set_draft_phase(DraftPhase.NOMINATION)
|
await self.set_draft_phase(DraftPhase.NOMINATING)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{
|
{
|
||||||
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
|
||||||
"payload": {"phase": self.draft_state.phase},
|
"payload": {"phase": self.draft_state.phase},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -214,7 +207,8 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{
|
{
|
||||||
"type": DraftMessage.CONFIRM_DETERMINE_DRAFT_ORDER,
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
|
||||||
"payload": {"draft_order": self.draft_state.draft_order},
|
"payload": {"draft_order": self.draft_state.draft_order},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -224,31 +218,55 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{
|
{
|
||||||
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
|
||||||
"payload": {"phase": self.draft_state.phase},
|
"payload": {"phase": self.draft_state.phase},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Broadcast Handlers ===
|
# === Broadcast Handlers ===
|
||||||
|
|
||||||
|
async def broadcast_admin(self, event):
|
||||||
|
await self._dispatch_broadcast(event)
|
||||||
|
|
||||||
|
|
||||||
class DraftParticipantConsumer(DraftConsumerBase):
|
class DraftParticipantConsumer(DraftConsumerBase):
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
await super().connect()
|
await super().connect()
|
||||||
|
|
||||||
self.draft_state.connect_user(self.user.username)
|
self.draft_state.connect_participant(self.user.username)
|
||||||
|
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
self.group_names.session,
|
||||||
|
{
|
||||||
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM,
|
||||||
|
"payload": {
|
||||||
|
"user": self.user.username,
|
||||||
|
"connected_participants": self.draft_state.connected_participants,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(
|
||||||
self.group_names.participant, self.channel_name
|
self.group_names.participant, self.channel_name
|
||||||
)
|
)
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
|
self.draft_state.disconnect_participant(self.user.username)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{"type": DraftMessage.INFORM_LEAVE_PARTICIPANT, "user": self.user.username},
|
{
|
||||||
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM,
|
||||||
|
"payload": {
|
||||||
|
"user": self.user.username,
|
||||||
|
"connected_participants": self.draft_state.connected_participants,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
await super().disconnect(close_code)
|
await super().disconnect(close_code)
|
||||||
self.draft_state.disconnect_user(self.user.username)
|
self.draft_state.disconnect_participant(self.user.username)
|
||||||
await self.channel_layer.group_discard(
|
await self.channel_layer.group_discard(
|
||||||
self.group_names.session, self.channel_name
|
self.group_names.session, self.channel_name
|
||||||
)
|
)
|
||||||
@@ -258,24 +276,11 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
|||||||
|
|
||||||
async def receive_json(self, content):
|
async def receive_json(self, content):
|
||||||
await super().receive_json(content)
|
await super().receive_json(content)
|
||||||
event_type = content.get("type")
|
|
||||||
user = self.scope["user"]
|
|
||||||
|
|
||||||
if event_type == DraftMessage.REQUEST_JOIN_PARTICIPANT:
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
self.group_names.admin,
|
|
||||||
{"type": DraftMessage.REQUEST_JOIN_PARTICIPANT, "user": user},
|
|
||||||
)
|
|
||||||
|
|
||||||
# === Broadcast handlers ===
|
# === Broadcast handlers ===
|
||||||
|
|
||||||
async def request_join_participant(self, event):
|
async def broadcast_participant(self, event):
|
||||||
await self.send_json(
|
await self._dispatch_broadcast(event)
|
||||||
{
|
|
||||||
"type": event["type"],
|
|
||||||
"user": event["user"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# === Draft ===
|
# === Draft ===
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class DraftCacheKeys:
|
|||||||
@property
|
@property
|
||||||
def draft_order(self):
|
def draft_order(self):
|
||||||
return f"{self.prefix}:draft_order"
|
return f"{self.prefix}:draft_order"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def draft_index(self):
|
||||||
|
return f"{self.prefix}:draft_index"
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
# def state(self):
|
# def state(self):
|
||||||
@@ -64,30 +68,30 @@ class DraftStateManager:
|
|||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.keys = DraftCacheKeys(session_id)
|
self.keys = DraftCacheKeys(session_id)
|
||||||
self._phase = self.cache.get(self.keys.phase, DraftPhase.WAITING)
|
self._initial_phase = self.cache.get(self.keys.phase, DraftPhase.WAITING.value)
|
||||||
|
|
||||||
|
|
||||||
# === Phase Management ===
|
# === Phase Management ===
|
||||||
@property
|
@property
|
||||||
def phase(self) -> str:
|
def phase(self) -> str:
|
||||||
return str(self.cache.get(self.keys.phase, self._phase))
|
return str(self.cache.get(self.keys.phase, self._initial_phase))
|
||||||
|
|
||||||
@phase.setter
|
@phase.setter
|
||||||
def phase(self, new_phase: DraftPhase):
|
def phase(self, new_phase: DraftPhase):
|
||||||
self.cache.set(self.keys.phase, new_phase)
|
self.cache.set(self.keys.phase, new_phase.value)
|
||||||
|
|
||||||
# === Connected Users ===
|
# === Connected Users ===
|
||||||
@property
|
@property
|
||||||
def connected_users(self) -> list[str]:
|
def connected_participants(self) -> list[str]:
|
||||||
return json.loads(self.cache.get(self.keys.connected_users) or "[]")
|
return json.loads(self.cache.get(self.keys.connected_users) or "[]")
|
||||||
|
|
||||||
def connect_user(self, username: str):
|
def connect_participant(self, username: str):
|
||||||
users = set(self.connected_users)
|
users = set(self.connected_participants)
|
||||||
users.add(username)
|
users.add(username)
|
||||||
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
||||||
|
|
||||||
def disconnect_user(self, username: str):
|
def disconnect_participant(self, username: str):
|
||||||
users = set(self.connected_users)
|
users = set(self.connected_participants)
|
||||||
users.discard(username)
|
users.discard(username)
|
||||||
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
||||||
|
|
||||||
@@ -102,6 +106,14 @@ class DraftStateManager:
|
|||||||
return
|
return
|
||||||
self.cache.set(self.keys.draft_order,json.dumps(draft_order))
|
self.cache.set(self.keys.draft_order,json.dumps(draft_order))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def draft_index(self):
|
||||||
|
return self.cache.get(self.keys.draft_index,0)
|
||||||
|
|
||||||
|
@draft_index.setter
|
||||||
|
def draft_index(self, draft_index: int):
|
||||||
|
self.cache.set(self.keys.draft_index, int(draft_index))
|
||||||
|
|
||||||
# === Current Nomination / Bid ===
|
# === Current Nomination / Bid ===
|
||||||
def start_nomination(self, movie_id: int):
|
def start_nomination(self, movie_id: int):
|
||||||
self.cache.set(self.keys.current_movie, movie_id)
|
self.cache.set(self.keys.current_movie, movie_id)
|
||||||
@@ -132,7 +144,8 @@ class DraftStateManager:
|
|||||||
return {
|
return {
|
||||||
"phase": self.phase,
|
"phase": self.phase,
|
||||||
"draft_order": self.draft_order,
|
"draft_order": self.draft_order,
|
||||||
"connected_users": self.connected_users,
|
"draft_index": self.draft_index,
|
||||||
|
"connected_participants": self.connected_participants,
|
||||||
# "current_movie": self.cache.get(self.keys.current_movie),
|
# "current_movie": self.cache.get(self.keys.current_movie),
|
||||||
# "bids": self.get_bids(),
|
# "bids": self.get_bids(),
|
||||||
# "timer_end": self.get_timer_end(),
|
# "timer_end": self.get_timer_end(),
|
||||||
|
|||||||
@@ -1,53 +1,13 @@
|
|||||||
// DraftAdmin.jsx
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||||
import { DraftMessage, DraftPhases, DraftPhase } from '../constants.js';
|
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||||
import { fetchDraftDetails } from "../common/utils.js"
|
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
|
||||||
|
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
|
||||||
const ParticipantList = ({ socket, participants, draftOrder }) => {
|
import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
|
||||||
const [connectedParticipants, setConnectedParticipants] = useState([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMessage = async ({ data }) => {
|
|
||||||
const message = JSON.parse(data)
|
|
||||||
const { type, payload } = message
|
|
||||||
console.log('socket changed', message)
|
|
||||||
if (payload?.connected_participants) {
|
|
||||||
const { connected_participants } = payload
|
|
||||||
setConnectedParticipants(connected_participants)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.addEventListener("message", handleMessage)
|
|
||||||
return () => {
|
|
||||||
socket.removeEventListener("message", handleMessage)
|
|
||||||
}
|
|
||||||
}, [socket])
|
|
||||||
|
|
||||||
const ListTag = draftOrder.length > 0 ? "ol" : "ul"
|
|
||||||
console.log
|
|
||||||
const listItems = draftOrder.length > 0 ? draftOrder.map(d => participants.find(p => p.username == d)) : participants
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="participant-list-container">
|
|
||||||
<label>Particpants</label>
|
|
||||||
<ListTag className="participant-list">
|
|
||||||
{listItems.map((p, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<span>{p?.full_name}</span>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
`ms-2 stop-light ${connectedParticipants.includes(p?.username) ? "success" : "danger"}`
|
|
||||||
}
|
|
||||||
></div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ListTag>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) => {
|
const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) => {
|
||||||
return (
|
return (
|
||||||
@@ -57,14 +17,14 @@ const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) =
|
|||||||
<div className="change-phase"><button onClick={prevPhaseHandler}><i className="bi bi-chevron-left"></i></button></div>
|
<div className="change-phase"><button onClick={prevPhaseHandler}><i className="bi bi-chevron-left"></i></button></div>
|
||||||
<ol>
|
<ol>
|
||||||
{
|
{
|
||||||
DraftPhases.map((p) => (
|
DraftPhasesOrdered.map((p) => (
|
||||||
<li key={p} className={p === draftPhase ? "current-phase" : ""}>
|
<li key={p} className={p === draftPhase ? "current-phase" : ""}>
|
||||||
<span>{p}</span>
|
<span>{DraftPhaseLabel[p]}</span>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ol>
|
</ol>
|
||||||
<div className="change-phase"><button onClick={nextPhaseHandler}><i className="bi bi-chevron-right"></i></button></div>
|
<div className="change-phase"><button onClick={nextPhaseHandler}><i className="bi bi-chevron-right"></i></button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -72,86 +32,84 @@ const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) =
|
|||||||
|
|
||||||
export const DraftAdmin = ({ draftSessionId }) => {
|
export const DraftAdmin = ({ draftSessionId }) => {
|
||||||
const socket = useWebSocket();
|
const socket = useWebSocket();
|
||||||
const [connectedParticipants, setConnectedParticipants] = useState([]);
|
|
||||||
const [draftDetails, setDraftDetails] = useState();
|
const [draftDetails, setDraftDetails] = useState();
|
||||||
const [participants, setParticipants] = React.useState([]);
|
const [draftState, setDraftState] = useState({})
|
||||||
const [draftPhase, setDraftPhase] = useState();
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const [draftOrder, setDraftOrder] = useState([]);
|
|
||||||
console.log(socket)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDraftDetails(draftSessionId)
|
fetchDraftDetails(draftSessionId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log("Fetched draft data", data)
|
console.log("Fetched draft data", data)
|
||||||
setParticipants(data.participants)
|
setDraftDetails(data)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(()=>{
|
||||||
|
if (!socket) return;
|
||||||
|
const openHandler = (event)=>{
|
||||||
|
console.log('Websocket Opened')
|
||||||
|
}
|
||||||
|
const closeHandler = (event)=>{
|
||||||
|
console.log('Websocket Closed')
|
||||||
|
}
|
||||||
|
socket.addEventListener('open', openHandler );
|
||||||
|
socket.addEventListener('close', closeHandler );
|
||||||
|
return ()=>{
|
||||||
|
socket.removeEventListener('open', openHandler );
|
||||||
|
socket.removeEventListener('close', closeHandler );
|
||||||
|
}
|
||||||
|
}, [socket])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
|
|
||||||
const handleMessage = (event) => {
|
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||||
const message = JSON.parse(event.data)
|
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||||
const { type, payload } = message;
|
socket.addEventListener('message', draftStatusMessageHandler );
|
||||||
console.log(type, event)
|
socket.addEventListener('message', userIdentifyMessageHandler );
|
||||||
if (!payload) return
|
|
||||||
if (type == DraftMessage.REQUEST.JOIN_PARTICIPANT) {
|
|
||||||
console.log('join request', data)
|
|
||||||
}
|
|
||||||
if (payload.phase) {
|
|
||||||
console.log('phase_change')
|
|
||||||
setDraftPhase(payload.phase)
|
|
||||||
}
|
|
||||||
if (payload.draft_order) {
|
|
||||||
console.log('draft_order', payload.draft_order)
|
|
||||||
setDraftOrder(payload.draft_order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.addEventListener('message', handleMessage);
|
|
||||||
|
|
||||||
socket.onclose = (event) => {
|
|
||||||
console.log('Websocket Closed')
|
|
||||||
socket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.removeEventListener('message', handleMessage)
|
socket.removeEventListener('message', draftStatusMessageHandler)
|
||||||
socket.close();
|
socket.removeEventListener('message', userIdentifyMessageHandler );
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const handlePhaseChange = (target) => {
|
const handlePhaseChange = (target) => {
|
||||||
let destination
|
let destination
|
||||||
const origin = draftPhase
|
const origin = draftState.phase
|
||||||
if (target == "next") {
|
const originPhaseIndex = DraftPhasesOrdered.findIndex(i => i == origin)
|
||||||
console.log(DraftPhase)
|
console.log('origin phase index', originPhaseIndex)
|
||||||
console.log("phase to be changed", origin, target, DraftPhase.WAITING)
|
if (target == "next" && originPhaseIndex < DraftPhasesOrdered.length) {
|
||||||
if (origin == "waiting"){
|
destination = DraftPhasesOrdered[originPhaseIndex + 1]
|
||||||
destination = DraftPhase.DETERMINE_ORDER
|
|
||||||
} else if (origin == "determine_order"){
|
|
||||||
destination = DraftPhase.NOMINATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (target=="previous") {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
else if (target == "previous" && originPhaseIndex > 0) {
|
||||||
if (!destination) {return}
|
destination = DraftPhasesOrdered[originPhaseIndex - 1]
|
||||||
|
}
|
||||||
|
console.log(destination)
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{ type: DraftMessage.REQUEST.PHASE_CHANGE, origin, destination }
|
{ type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStartDraft = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdvanceDraft = () => {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify(
|
||||||
|
{ type: DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRequestDraftSummary = () => {
|
const handleRequestDraftSummary = () => {
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{ type: DraftMessage.REQUEST.DRAFT_STATUS }
|
{ type: DraftMessage.STATUS_SYNC_REQUEST }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -169,12 +127,14 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ParticipantList
|
<ParticipantList
|
||||||
socket={socket}
|
draftState={draftState}
|
||||||
participants={participants}
|
draftDetails={draftDetails}
|
||||||
draftOrder={draftOrder}
|
isAdmin={true}
|
||||||
/>
|
/>
|
||||||
|
<button onClick={handleAdvanceDraft} className="btn btn-primary">Advance Draft</button>
|
||||||
|
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
|
||||||
|
|
||||||
<DraftPhaseDisplay draftPhase={draftPhase} nextPhaseHandler={ ()=>{handlePhaseChange('next')}} prevPhaseHandler= {() => {handlePhaseChange('previous')}}></DraftPhaseDisplay>
|
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
21
frontend/src/apps/draft/common/DraftMoviePool.jsx
Normal file
21
frontend/src/apps/draft/common/DraftMoviePool.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { isEmptyObject } from "./utils";
|
||||||
|
|
||||||
|
export const DraftMoviePool = ({ draftDetails }) => {
|
||||||
|
if(isEmptyObject(draftDetails)) {return}
|
||||||
|
const {movies} = draftDetails
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="movie-pool-container">
|
||||||
|
<ul>
|
||||||
|
{movies.map(m => (
|
||||||
|
<li key={m.id}>
|
||||||
|
<a href={`/api/movie/${m.id}/detail`}>
|
||||||
|
{m.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
33
frontend/src/apps/draft/common/ParticipantList.jsx
Normal file
33
frontend/src/apps/draft/common/ParticipantList.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { fetchDraftDetails, isEmptyObject } from "../common/utils.js"
|
||||||
|
|
||||||
|
export const ParticipantList = ({ isAdmin, draftState, draftDetails }) => {
|
||||||
|
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return }
|
||||||
|
const { draft_order, draft_index, connected_participants } = draftState
|
||||||
|
const { participants } = draftDetails
|
||||||
|
|
||||||
|
const ListTag = draft_order.length > 0 ? "ol" : "ul"
|
||||||
|
console.log
|
||||||
|
const listItems = draft_order.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="participant-list-container">
|
||||||
|
<label>Particpants</label>
|
||||||
|
<ListTag className="participant-list">
|
||||||
|
{listItems.map((p, i) => (
|
||||||
|
<li key={i} className={`${i == draft_index ? "fw-bold" : ""}`}>
|
||||||
|
<span>{p?.full_name}</span>
|
||||||
|
{isAdmin ? (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
`ms-2 stop-light ${connected_participants.includes(p?.username) ? "success" : "danger"}`
|
||||||
|
}
|
||||||
|
></div>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ListTag>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DraftMessage } from "../constants"
|
||||||
|
|
||||||
export async function fetchDraftDetails(draftSessionId) {
|
export async function fetchDraftDetails(draftSessionId) {
|
||||||
return fetch(`/api/draft/${draftSessionId}/`)
|
return fetch(`/api/draft/${draftSessionId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -26,4 +28,40 @@ export async function fetchMovieDetails(draftSessionId) {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Error fetching draft details", err)
|
console.error("Error fetching draft details", err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmptyObject(obj) {
|
||||||
|
return obj == null || (Object.keys(obj).length === 0 && obj.constructor === Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
const { type, payload } = message;
|
||||||
|
console.log("Message: ", type, event?.data)
|
||||||
|
|
||||||
|
if (!payload) return
|
||||||
|
const {connected_participants, phase, draft_order, draft_index} = payload
|
||||||
|
|
||||||
|
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
||||||
|
setDraftState(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraftState(prev=>({
|
||||||
|
...prev,
|
||||||
|
...(connected_participants ? { connected_participants } : {}),
|
||||||
|
...(draft_order ? { draft_order } : {}),
|
||||||
|
...(draft_index ? { draft_index } : {}),
|
||||||
|
...(phase ? { phase: Number(phase) } : {}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleUserIdentifyMessages = (event, setUser) => {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
const { type, payload } = message;
|
||||||
|
console.log("Message: ", type, event?.data)
|
||||||
|
|
||||||
|
if (!payload) return
|
||||||
|
const {current_user} = payload
|
||||||
|
setUser(current_user)
|
||||||
|
}
|
||||||
@@ -1,51 +1,48 @@
|
|||||||
|
// AUTO-GENERATED. Do not edit by hand.
|
||||||
|
// Run: python scripts/generate_js_constants.py
|
||||||
|
|
||||||
|
|
||||||
export const DraftMessage = {
|
export const DraftMessage = {
|
||||||
// Server to Client
|
PARTICIPANT_JOIN_REQUEST: "participant.join.request",
|
||||||
INFORM: {
|
PARTICIPANT_JOIN_CONFIRM: "participant.join.confirm",
|
||||||
PHASE_CHANGE: "inform.phase.change",
|
PARTICIPANT_JOIN_REJECT: "participant.join.reject",
|
||||||
PHASE: "inform.phase",
|
PARTICIPANT_LEAVE_INFORM: "participant.leave.inform",
|
||||||
STATUS: "inform.status",
|
USER_JOIN_INFORM: "user.join.inform",
|
||||||
JOIN_USER: "inform.join.user",
|
USER_LEAVE_INFORM: "user.leave.inform",
|
||||||
DRAFT_STATUS: "inform.draft_status"
|
USER_IDENTIFICATION_INFORM: "user.identification.inform",
|
||||||
},
|
PHASE_CHANGE_INFORM: "phase.change.inform",
|
||||||
|
PHASE_CHANGE_REQUEST: "phase.change.request",
|
||||||
// Client to Server
|
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
|
||||||
REQUEST: {
|
STATUS_SYNC_REQUEST: "status.sync.request",
|
||||||
PHASE_CHANGE: "request.phase.change",
|
STATUS_SYNC_INFORM: "status.sync.inform",
|
||||||
INFORM_STATUS: "request.inform.status",
|
DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request",
|
||||||
JOIN_PARTICIPANT: "request.join.participant",
|
DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm",
|
||||||
JOIN_ADMIN: "request.join.admin",
|
ORDER_DETERMINE_REQUEST: "order.determine.request",
|
||||||
DETERMINE_DRAFT_ORDER: "request.determine.draft_order",
|
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
||||||
DRAFT_STATUS: "request.draft_status"
|
BID_START_INFORM: "bid.start.inform",
|
||||||
},
|
BID_PLACE_REQUEST: "bid.place.request",
|
||||||
|
BID_UPDATE_INFORM: "bid.update.inform",
|
||||||
// Confirmation messages (Server to Client)
|
BID_END_INFORM: "bid.end.inform",
|
||||||
CONFIRM: {
|
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",
|
||||||
PHASE_CHANGE: "confirm.phase.change",
|
NOMINATION_CONFIRM: "nomination.submit.confirm",
|
||||||
JOIN_PARTICIPANT: "confirm.join.participant",
|
|
||||||
JOIN_ADMIN: "confirm.join.admin",
|
|
||||||
DETERMINE_DRAFT_ORDER: "confirm.determine.draft_order",
|
|
||||||
},
|
|
||||||
|
|
||||||
// Client-side notification (to server)
|
|
||||||
NOTIFY: {
|
|
||||||
JOIN_USER: "notify.join.user",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DraftPhase = {
|
export const DraftPhase = {
|
||||||
WAITING: 0,
|
WAITING: 10,
|
||||||
DETERMINE_ORDER: 10,
|
DETERMINE_ORDER: 20,
|
||||||
NOMINATION: 20,
|
NOMINATING: 30,
|
||||||
BIDDING: 30,
|
BIDDING: 40,
|
||||||
AWARD: 40,
|
AWARDING: 50,
|
||||||
FINALIZE: 50,
|
FINALIZING: 60,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DraftPhases = [
|
export const DraftPhaseLabel = {
|
||||||
"waiting",
|
[DraftPhase.WAITING]: "waiting",
|
||||||
"determine_order",
|
[DraftPhase.DETERMINE_ORDER]: "determine_order",
|
||||||
"nomination",
|
[DraftPhase.NOMINATING]: "nominating",
|
||||||
"bidding",
|
[DraftPhase.BIDDING]: "bidding",
|
||||||
"award",
|
[DraftPhase.AWARDING]: "awarding",
|
||||||
"finalize",
|
[DraftPhase.FINALIZING]: "finalizing",
|
||||||
]
|
};
|
||||||
|
|
||||||
|
export const DraftPhasesOrdered = [DraftPhase.WAITING, DraftPhase.DETERMINE_ORDER, DraftPhase.NOMINATING, DraftPhase.BIDDING, DraftPhase.AWARDING, DraftPhase.FINALIZING];
|
||||||
|
|||||||
@@ -3,29 +3,18 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||||
import { DraftMessage, DraftPhases } from '../constants.js';
|
import { DraftMessage, DraftPhases} from '../constants.js';
|
||||||
import { fetchDraftDetails } from "../common/utils.js"
|
import { fetchDraftDetails } from "../common/utils.js";
|
||||||
|
import { DraftMoviePool } from "../common/DraftMoviePool.jsx";
|
||||||
|
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||||
|
import { handleDraftStatusMessages } from '../common/utils.js'
|
||||||
|
|
||||||
const DraftMoviePool = ({ movies }) => {
|
|
||||||
return (
|
|
||||||
<div className="movie-pool-container">
|
|
||||||
<ul>
|
|
||||||
{movies.map(m => (
|
|
||||||
<li id={m?.id}>
|
|
||||||
<a href={`/api/movie/${m.id}/detail`}>
|
|
||||||
{m.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DraftParticipant = ({ draftSessionId }) => {
|
export const DraftParticipant = ({ draftSessionId }) => {
|
||||||
const socket = useWebSocket();
|
const socket = useWebSocket();
|
||||||
const [participants, setParticipants] = useState([]);
|
const [draftState, setDraftState] = useState({});
|
||||||
const [draftPhase, setDraftPhase] = useState();
|
const [draftDetails, setDraftDetails] = useState({});
|
||||||
|
|
||||||
const [movies, setMovies] = useState([]);
|
const [movies, setMovies] = useState([]);
|
||||||
console.log(socket)
|
console.log(socket)
|
||||||
|
|
||||||
@@ -34,65 +23,41 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log("Fetched draft data", data)
|
console.log("Fetched draft data", data)
|
||||||
setMovies(data.movies)
|
setMovies(data.movies)
|
||||||
|
setDraftDetails(data)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [draftSessionId])
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if (!socket) return;
|
||||||
|
socket.onclose = (event) => {
|
||||||
|
console.log('Websocket Closed')
|
||||||
|
}
|
||||||
|
}, [socket])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
else {
|
|
||||||
console.warn("socket doesn't exist")
|
|
||||||
}
|
|
||||||
console.log('socket created', socket)
|
|
||||||
|
|
||||||
const handleMessage = (event) => {
|
const handler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||||
const message = JSON.parse(event.data)
|
socket.addEventListener('message', handler );
|
||||||
const { type, payload } = message;
|
|
||||||
console.log(type, event)
|
|
||||||
if (type == DraftMessage.REQUEST.JOIN_PARTICIPANT) {
|
|
||||||
console.log('join request', data)
|
|
||||||
}
|
|
||||||
else if (type == DraftMessage.CONFIRM.JOIN_PARTICIPANT) {
|
|
||||||
setConnectedParticipants(data.connected_participants)
|
|
||||||
}
|
|
||||||
else if (type == DraftMessage.CONFIRM.PHASE_CHANGE || type == DraftMessage.INFORM.PHASE) {
|
|
||||||
console.log('phase_change')
|
|
||||||
setDraftPhase(payload.phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.addEventListener('message', handleMessage);
|
|
||||||
|
|
||||||
socket.onclose = (event) => {
|
|
||||||
console.log('Websocket Closed')
|
|
||||||
socket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.removeEventListener('message', handleMessage)
|
socket.addEventListener('message', handler );
|
||||||
socket.close();
|
socket.close();
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const handlePhaseChange = (destinationPhase) => {
|
|
||||||
socket.send(
|
|
||||||
JSON.stringify({ type: DraftMessage.REQUEST.PHASE_CHANGE, "destination": destinationPhase })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const handleRequestDraftSummary = () => {
|
|
||||||
socket.send(JSON.stringify({ type: 'request_summary' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container draft-panel">
|
<div className="container draft-panel">
|
||||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
||||||
<h3>Draft Panel</h3>
|
<h3>Draft Panel</h3>
|
||||||
<WebSocketStatus socket={socket} />
|
<WebSocketStatus socket={socket} />
|
||||||
</div>
|
</div>
|
||||||
|
<ParticipantList
|
||||||
<DraftMoviePool movies={movies}></DraftMoviePool>
|
draftState={draftState}
|
||||||
|
draftDetails={draftDetails}
|
||||||
|
isAdmin={false}
|
||||||
|
/>
|
||||||
|
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
103
scripts/generate_js_constants.py
Normal file
103
scripts/generate_js_constants.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from enum import Enum, IntEnum, StrEnum
|
||||||
|
|
||||||
|
# Adjust these for your project
|
||||||
|
PY_MODULE = "draft.constants" # where your enums live
|
||||||
|
OUTPUT_PATH = "frontend/src/apps/draft/constants.js"
|
||||||
|
|
||||||
|
# Optionally allow running from any cwd
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PROJECT_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, ".."))
|
||||||
|
if PROJECT_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, PROJECT_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def js_quote(s: str) -> str:
|
||||||
|
return s.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
|
||||||
|
|
||||||
|
def titleize(name: str) -> str:
|
||||||
|
# e.g., "DETERMINE_ORDER" -> "Determine Order"
|
||||||
|
return name.replace("_", " ").title()
|
||||||
|
|
||||||
|
|
||||||
|
def emit_header():
|
||||||
|
return "// AUTO-GENERATED. Do not edit by hand.\n" \
|
||||||
|
"// Run: python scripts/generate_js_constants.py\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def emit_str_enum(name: str, enum_cls) -> str:
|
||||||
|
"""
|
||||||
|
Emit a JS object for StrEnum:
|
||||||
|
export const DraftMessage = { KEY: "value", ... };
|
||||||
|
"""
|
||||||
|
lines = [f"export const {name} = {{"] # ESM export
|
||||||
|
for member in enum_cls:
|
||||||
|
lines.append(f' {member.name}: "{js_quote(member.value)}",')
|
||||||
|
lines.append("};\n")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def emit_int_enum(name: str, enum_cls) -> str:
|
||||||
|
"""
|
||||||
|
Emit a JS object + labels + ordered list for IntEnum:
|
||||||
|
export const DraftPhase = { KEY: number, ... };
|
||||||
|
export const DraftPhaseLabel = { [number]: "Pretty", ... };
|
||||||
|
export const DraftPhasesOrdered = [numbers...];
|
||||||
|
"""
|
||||||
|
lines = [f"export const {name} = {{"] # ESM export
|
||||||
|
items = list(enum_cls)
|
||||||
|
# object map
|
||||||
|
for member in items:
|
||||||
|
lines.append(f" {member.name}: {int(member.value)},")
|
||||||
|
lines.append("};\n")
|
||||||
|
|
||||||
|
# label map (use .pretty_name if you added it; else derive from name or __str__)
|
||||||
|
lines.append(f"export const {name}Label = {{")
|
||||||
|
for member in items:
|
||||||
|
if hasattr(member, "pretty_name"):
|
||||||
|
label = getattr(member, "pretty_name")
|
||||||
|
else:
|
||||||
|
# fall back: __str__ if you overload it, else Title Case of name
|
||||||
|
label = str(member)
|
||||||
|
if label == f"{enum_cls.__name__}.{member.name}":
|
||||||
|
label = titleize(member.name)
|
||||||
|
lines.append(f' [{name}.{member.name}]: "{js_quote(label)}",')
|
||||||
|
lines.append("};\n")
|
||||||
|
|
||||||
|
# ordered list
|
||||||
|
ordered = sorted(items, key=lambda m: int(m.value))
|
||||||
|
ordered_vals = ", ".join(f"{name}.{m.name}" for m in ordered)
|
||||||
|
lines.append(f"export const {name}sOrdered = [{ordered_vals}];\n")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
mod = importlib.import_module(PY_MODULE)
|
||||||
|
out = [emit_header()]
|
||||||
|
|
||||||
|
# Pick which enums to export. You can filter here if you don’t want all.
|
||||||
|
for name, obj in inspect.getmembers(mod):
|
||||||
|
ignore_classes = [Enum, IntEnum, StrEnum]
|
||||||
|
if inspect.isclass(obj) and issubclass(obj, Enum) and not obj in ignore_classes:
|
||||||
|
# Skip helper classes that aren’t actual Enums
|
||||||
|
if name.startswith("_"):
|
||||||
|
continue
|
||||||
|
if issubclass(obj, IntEnum):
|
||||||
|
out.append(emit_int_enum(name, obj))
|
||||||
|
else:
|
||||||
|
out.append(emit_str_enum(name, obj))
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
||||||
|
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(out))
|
||||||
|
|
||||||
|
print(f"✅ Wrote {OUTPUT_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user