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")
|
||||
|
||||
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:
|
||||
# Server
|
||||
INFORM_PHASE_CHANGE = "inform.phase.change"
|
||||
CONFIRM_PHASE_CHANGE = "confirm.phase.change"
|
||||
INFORM_PHASE = "inform.phase"
|
||||
INFORM_DRAFT_STATUS = "inform.draft_status"
|
||||
class DraftMessage(StrEnum):
|
||||
# Participant
|
||||
PARTICIPANT_JOIN_REQUEST = "participant.join.request" # client -> server
|
||||
PARTICIPANT_JOIN_CONFIRM = "participant.join.confirm" # server -> client
|
||||
PARTICIPANT_JOIN_REJECT = "participant.join.reject" # server -> client
|
||||
PARTICIPANT_LEAVE_INFORM = "participant.leave.inform" # server -> client (broadcast)
|
||||
|
||||
# Client
|
||||
REQUEST_PHASE_CHANGE = "request.phase.change"
|
||||
REQUEST_DRAFT_STATUS = "request.draft_status"
|
||||
# User presence
|
||||
USER_JOIN_INFORM = "user.join.inform" # server -> client
|
||||
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
|
||||
## Server
|
||||
INFORM_JOIN_USER = "inform.join.user"
|
||||
REQUEST_JOIN_PARTICIPANT = "request.join.participant"
|
||||
REQUEST_JOIN_ADMIN = "request.join.admin"
|
||||
INFORM_LEAVE_PARTICIPANT = "inform.leave.participant"
|
||||
# Phase control
|
||||
PHASE_CHANGE_INFORM = "phase.change.inform" # server -> client (target phase payload)
|
||||
PHASE_CHANGE_REQUEST = "phase.change.request" # server -> client (target phase payload)
|
||||
PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload)
|
||||
|
||||
## Client
|
||||
NOTIFY_JOIN_USER = "notify.join.user"
|
||||
CONFIRM_JOIN_PARTICIPANT = "confirm.join.participant"
|
||||
REJECT_JOIN_PARTICIPANT = "reject.join.participant"
|
||||
CONFIRM_JOIN_ADMIN = "confirm.join.admin"
|
||||
# Status / sync
|
||||
STATUS_SYNC_REQUEST = "status.sync.request" # client -> server
|
||||
STATUS_SYNC_INFORM = "status.sync.inform" # server -> client (full/partial state)
|
||||
|
||||
# Determine Order
|
||||
## Server
|
||||
CONFIRM_DETERMINE_DRAFT_ORDER = "confirm.determine.draft_order"
|
||||
## Client
|
||||
REQUEST_DETERMINE_DRAFT_ORDER = "request.determine.draft_order"
|
||||
DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request"
|
||||
DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm"
|
||||
|
||||
# Order determination
|
||||
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):
|
||||
WAITING = 0
|
||||
DETERMINE_ORDER = 10
|
||||
NOMINATION = 20
|
||||
BIDDING = 30
|
||||
AWARD = 40
|
||||
FINALIZE = 50
|
||||
WAITING = 10
|
||||
DETERMINE_ORDER = 20
|
||||
NOMINATING = 30
|
||||
BIDDING = 40
|
||||
AWARDING = 50
|
||||
FINALIZING = 60
|
||||
|
||||
def __str__(self):
|
||||
return self.name.lower()
|
||||
|
||||
@@ -14,6 +14,10 @@ from draft.constants import (
|
||||
)
|
||||
from draft.state import DraftCacheKeys, DraftStateManager
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__) # __name__ = module path
|
||||
|
||||
|
||||
import random
|
||||
|
||||
@@ -40,18 +44,21 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
|
||||
self.user = self.scope["user"]
|
||||
if not self.should_accept_user():
|
||||
await self.send_json(
|
||||
await self.channel_layer.send(
|
||||
self.channel_name,
|
||||
{
|
||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
||||
"user": self.user.username,
|
||||
"type": "direct.message",
|
||||
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
|
||||
"payload":{"current_user": self.user.username}
|
||||
}
|
||||
)
|
||||
await self.close()
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
self.group_names.admin,
|
||||
{
|
||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
||||
"user": self.user.username,
|
||||
"type": "broadcast.admin",
|
||||
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
|
||||
"payload":{"user": self.user.username}
|
||||
},
|
||||
)
|
||||
return
|
||||
@@ -62,81 +69,56 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
)
|
||||
await self.channel_layer.group_send(
|
||||
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(),
|
||||
},
|
||||
)
|
||||
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:
|
||||
return self.user.is_authenticated
|
||||
|
||||
async def receive_json(self, content):
|
||||
event_type = content.get("type")
|
||||
if event_type == DraftMessage.REQUEST_DRAFT_STATUS:
|
||||
if event_type == DraftMessage.STATUS_SYNC_REQUEST:
|
||||
await self.send_json(
|
||||
{
|
||||
"type": DraftMessage.INFORM_DRAFT_STATUS,
|
||||
"type": DraftMessage.STATUS_SYNC_INFORM,
|
||||
"payload": self.get_draft_status(),
|
||||
}
|
||||
)
|
||||
|
||||
async def inform_leave_participant(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
"payload": {
|
||||
"participants": [user.username for user in self.draft_participants],
|
||||
"connected_participants": self.draft_state.connected_users,
|
||||
},
|
||||
}
|
||||
)
|
||||
# Broadcast Handlers
|
||||
async def direct_message(self, event):
|
||||
await self._dispatch_broadcast(event)
|
||||
|
||||
async def inform_join_user(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"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 broadcast_session(self, event):
|
||||
await self._dispatch_broadcast(event)
|
||||
|
||||
async def inform_draft_status(self, event):
|
||||
await self.send_json(
|
||||
{"type": event["type"], "payload": self.get_draft_status()}
|
||||
)
|
||||
async def _dispatch_broadcast(self, event):
|
||||
logger.info(f"dispatching message {event}")
|
||||
subtype = event.get("subtype")
|
||||
payload = event.get("payload", {})
|
||||
await self.send_json({"type": subtype, "payload": payload})
|
||||
|
||||
async def reject_join_participant(self, event):
|
||||
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): ...
|
||||
# === Methods ===
|
||||
|
||||
def get_draft_status(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -145,8 +127,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
"participants": [user.username for user in self.draft_participants],
|
||||
}
|
||||
|
||||
# === Broadcast handlers ===
|
||||
|
||||
# === DB Access ===
|
||||
@database_sync_to_async
|
||||
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
||||
@@ -177,29 +157,42 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
|
||||
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.REQUEST_PHASE_CHANGE
|
||||
event_type == DraftMessage.PHASE_CHANGE_REQUEST
|
||||
and content.get("destination") == DraftPhase.DETERMINE_ORDER
|
||||
):
|
||||
await self.determine_draft_order()
|
||||
|
||||
|
||||
if (
|
||||
event_type == DraftMessage.REQUEST_PHASE_CHANGE
|
||||
and content.get("destination") == DraftPhase.NOMINATION
|
||||
event_type == DraftMessage.PHASE_CHANGE_REQUEST
|
||||
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):
|
||||
return super().should_accept_user() and self.user.is_staff
|
||||
|
||||
# === Draft logic ===
|
||||
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(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
|
||||
"type": "broadcast.session",
|
||||
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
|
||||
"payload": {"phase": self.draft_state.phase},
|
||||
},
|
||||
)
|
||||
@@ -214,7 +207,8 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
await self.channel_layer.group_send(
|
||||
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},
|
||||
},
|
||||
)
|
||||
@@ -224,31 +218,55 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
|
||||
"type": "broadcast.session",
|
||||
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
|
||||
"payload": {"phase": self.draft_state.phase},
|
||||
},
|
||||
)
|
||||
|
||||
# === Broadcast Handlers ===
|
||||
|
||||
async def broadcast_admin(self, event):
|
||||
await self._dispatch_broadcast(event)
|
||||
|
||||
|
||||
class DraftParticipantConsumer(DraftConsumerBase):
|
||||
async def connect(self):
|
||||
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(
|
||||
self.group_names.participant, self.channel_name
|
||||
)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
self.draft_state.disconnect_participant(self.user.username)
|
||||
await self.channel_layer.group_send(
|
||||
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)
|
||||
self.draft_state.disconnect_user(self.user.username)
|
||||
self.draft_state.disconnect_participant(self.user.username)
|
||||
await self.channel_layer.group_discard(
|
||||
self.group_names.session, self.channel_name
|
||||
)
|
||||
@@ -258,24 +276,11 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
||||
|
||||
async def receive_json(self, 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 ===
|
||||
|
||||
async def request_join_participant(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
}
|
||||
)
|
||||
async def broadcast_participant(self, event):
|
||||
await self._dispatch_broadcast(event)
|
||||
|
||||
# === Draft ===
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ class DraftCacheKeys:
|
||||
@property
|
||||
def draft_order(self):
|
||||
return f"{self.prefix}:draft_order"
|
||||
|
||||
@property
|
||||
def draft_index(self):
|
||||
return f"{self.prefix}:draft_index"
|
||||
|
||||
# @property
|
||||
# def state(self):
|
||||
@@ -64,30 +68,30 @@ class DraftStateManager:
|
||||
self.session_id = session_id
|
||||
self.cache = cache
|
||||
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 ===
|
||||
@property
|
||||
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
|
||||
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 ===
|
||||
@property
|
||||
def connected_users(self) -> list[str]:
|
||||
def connected_participants(self) -> list[str]:
|
||||
return json.loads(self.cache.get(self.keys.connected_users) or "[]")
|
||||
|
||||
def connect_user(self, username: str):
|
||||
users = set(self.connected_users)
|
||||
def connect_participant(self, username: str):
|
||||
users = set(self.connected_participants)
|
||||
users.add(username)
|
||||
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
||||
|
||||
def disconnect_user(self, username: str):
|
||||
users = set(self.connected_users)
|
||||
def disconnect_participant(self, username: str):
|
||||
users = set(self.connected_participants)
|
||||
users.discard(username)
|
||||
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
||||
|
||||
@@ -102,6 +106,14 @@ class DraftStateManager:
|
||||
return
|
||||
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 ===
|
||||
def start_nomination(self, movie_id: int):
|
||||
self.cache.set(self.keys.current_movie, movie_id)
|
||||
@@ -132,7 +144,8 @@ class DraftStateManager:
|
||||
return {
|
||||
"phase": self.phase,
|
||||
"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),
|
||||
# "bids": self.get_bids(),
|
||||
# "timer_end": self.get_timer_end(),
|
||||
|
||||
@@ -1,53 +1,13 @@
|
||||
// DraftAdmin.jsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||
import { DraftMessage, DraftPhases, DraftPhase } from '../constants.js';
|
||||
import { fetchDraftDetails } from "../common/utils.js"
|
||||
|
||||
const ParticipantList = ({ socket, participants, draftOrder }) => {
|
||||
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
|
||||
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
|
||||
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
|
||||
import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
|
||||
|
||||
|
||||
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 }) => {
|
||||
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>
|
||||
<ol>
|
||||
{
|
||||
DraftPhases.map((p) => (
|
||||
DraftPhasesOrdered.map((p) => (
|
||||
<li key={p} className={p === draftPhase ? "current-phase" : ""}>
|
||||
<span>{p}</span>
|
||||
<span>{DraftPhaseLabel[p]}</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</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>
|
||||
)
|
||||
@@ -72,86 +32,84 @@ const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) =
|
||||
|
||||
export const DraftAdmin = ({ draftSessionId }) => {
|
||||
const socket = useWebSocket();
|
||||
const [connectedParticipants, setConnectedParticipants] = useState([]);
|
||||
const [draftDetails, setDraftDetails] = useState();
|
||||
const [participants, setParticipants] = React.useState([]);
|
||||
const [draftPhase, setDraftPhase] = useState();
|
||||
const [draftOrder, setDraftOrder] = useState([]);
|
||||
console.log(socket)
|
||||
const [draftState, setDraftState] = useState({})
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDraftDetails(draftSessionId)
|
||||
.then((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;
|
||||
|
||||
const handleMessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
const { type, payload } = message;
|
||||
console.log(type, event)
|
||||
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;
|
||||
}
|
||||
|
||||
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||
socket.addEventListener('message', draftStatusMessageHandler );
|
||||
socket.addEventListener('message', userIdentifyMessageHandler );
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', handleMessage)
|
||||
socket.close();
|
||||
socket.removeEventListener('message', draftStatusMessageHandler)
|
||||
socket.removeEventListener('message', userIdentifyMessageHandler );
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const handlePhaseChange = (target) => {
|
||||
let destination
|
||||
const origin = draftPhase
|
||||
if (target == "next") {
|
||||
console.log(DraftPhase)
|
||||
console.log("phase to be changed", origin, target, DraftPhase.WAITING)
|
||||
if (origin == "waiting"){
|
||||
destination = DraftPhase.DETERMINE_ORDER
|
||||
} else if (origin == "determine_order"){
|
||||
destination = DraftPhase.NOMINATION
|
||||
}
|
||||
}
|
||||
else if (target=="previous") {
|
||||
|
||||
const origin = draftState.phase
|
||||
const originPhaseIndex = DraftPhasesOrdered.findIndex(i => i == origin)
|
||||
console.log('origin phase index', originPhaseIndex)
|
||||
if (target == "next" && originPhaseIndex < DraftPhasesOrdered.length) {
|
||||
destination = DraftPhasesOrdered[originPhaseIndex + 1]
|
||||
}
|
||||
|
||||
if (!destination) {return}
|
||||
else if (target == "previous" && originPhaseIndex > 0) {
|
||||
destination = DraftPhasesOrdered[originPhaseIndex - 1]
|
||||
}
|
||||
console.log(destination)
|
||||
socket.send(
|
||||
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 = () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{ type: DraftMessage.REQUEST.DRAFT_STATUS }
|
||||
{ type: DraftMessage.STATUS_SYNC_REQUEST }
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -169,12 +127,14 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
||||
</div>
|
||||
|
||||
<ParticipantList
|
||||
socket={socket}
|
||||
participants={participants}
|
||||
draftOrder={draftOrder}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
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>
|
||||
);
|
||||
};
|
||||
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) {
|
||||
return fetch(`/api/draft/${draftSessionId}/`)
|
||||
.then((response) => {
|
||||
@@ -26,4 +28,40 @@ export async function fetchMovieDetails(draftSessionId) {
|
||||
.catch((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 = {
|
||||
// Server to Client
|
||||
INFORM: {
|
||||
PHASE_CHANGE: "inform.phase.change",
|
||||
PHASE: "inform.phase",
|
||||
STATUS: "inform.status",
|
||||
JOIN_USER: "inform.join.user",
|
||||
DRAFT_STATUS: "inform.draft_status"
|
||||
},
|
||||
|
||||
// Client to Server
|
||||
REQUEST: {
|
||||
PHASE_CHANGE: "request.phase.change",
|
||||
INFORM_STATUS: "request.inform.status",
|
||||
JOIN_PARTICIPANT: "request.join.participant",
|
||||
JOIN_ADMIN: "request.join.admin",
|
||||
DETERMINE_DRAFT_ORDER: "request.determine.draft_order",
|
||||
DRAFT_STATUS: "request.draft_status"
|
||||
},
|
||||
|
||||
// Confirmation messages (Server to Client)
|
||||
CONFIRM: {
|
||||
PHASE_CHANGE: "confirm.phase.change",
|
||||
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",
|
||||
},
|
||||
PARTICIPANT_JOIN_REQUEST: "participant.join.request",
|
||||
PARTICIPANT_JOIN_CONFIRM: "participant.join.confirm",
|
||||
PARTICIPANT_JOIN_REJECT: "participant.join.reject",
|
||||
PARTICIPANT_LEAVE_INFORM: "participant.leave.inform",
|
||||
USER_JOIN_INFORM: "user.join.inform",
|
||||
USER_LEAVE_INFORM: "user.leave.inform",
|
||||
USER_IDENTIFICATION_INFORM: "user.identification.inform",
|
||||
PHASE_CHANGE_INFORM: "phase.change.inform",
|
||||
PHASE_CHANGE_REQUEST: "phase.change.request",
|
||||
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
|
||||
STATUS_SYNC_REQUEST: "status.sync.request",
|
||||
STATUS_SYNC_INFORM: "status.sync.inform",
|
||||
DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request",
|
||||
DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm",
|
||||
ORDER_DETERMINE_REQUEST: "order.determine.request",
|
||||
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
||||
BID_START_INFORM: "bid.start.inform",
|
||||
BID_PLACE_REQUEST: "bid.place.request",
|
||||
BID_UPDATE_INFORM: "bid.update.inform",
|
||||
BID_END_INFORM: "bid.end.inform",
|
||||
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",
|
||||
NOMINATION_CONFIRM: "nomination.submit.confirm",
|
||||
};
|
||||
|
||||
export const DraftPhase = {
|
||||
WAITING: 0,
|
||||
DETERMINE_ORDER: 10,
|
||||
NOMINATION: 20,
|
||||
BIDDING: 30,
|
||||
AWARD: 40,
|
||||
FINALIZE: 50,
|
||||
}
|
||||
WAITING: 10,
|
||||
DETERMINE_ORDER: 20,
|
||||
NOMINATING: 30,
|
||||
BIDDING: 40,
|
||||
AWARDING: 50,
|
||||
FINALIZING: 60,
|
||||
};
|
||||
|
||||
export const DraftPhases = [
|
||||
"waiting",
|
||||
"determine_order",
|
||||
"nomination",
|
||||
"bidding",
|
||||
"award",
|
||||
"finalize",
|
||||
]
|
||||
export const DraftPhaseLabel = {
|
||||
[DraftPhase.WAITING]: "waiting",
|
||||
[DraftPhase.DETERMINE_ORDER]: "determine_order",
|
||||
[DraftPhase.NOMINATING]: "nominating",
|
||||
[DraftPhase.BIDDING]: "bidding",
|
||||
[DraftPhase.AWARDING]: "awarding",
|
||||
[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 { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||
import { DraftMessage, DraftPhases } from '../constants.js';
|
||||
import { fetchDraftDetails } from "../common/utils.js"
|
||||
import { DraftMessage, DraftPhases} from '../constants.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 }) => {
|
||||
const socket = useWebSocket();
|
||||
const [participants, setParticipants] = useState([]);
|
||||
const [draftPhase, setDraftPhase] = useState();
|
||||
const [draftState, setDraftState] = useState({});
|
||||
const [draftDetails, setDraftDetails] = useState({});
|
||||
|
||||
const [movies, setMovies] = useState([]);
|
||||
console.log(socket)
|
||||
|
||||
@@ -34,65 +23,41 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
.then((data) => {
|
||||
console.log("Fetched draft data", data)
|
||||
setMovies(data.movies)
|
||||
setDraftDetails(data)
|
||||
})
|
||||
}, [])
|
||||
}, [draftSessionId])
|
||||
|
||||
useEffect(()=>{
|
||||
if (!socket) return;
|
||||
socket.onclose = (event) => {
|
||||
console.log('Websocket Closed')
|
||||
}
|
||||
}, [socket])
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
else {
|
||||
console.warn("socket doesn't exist")
|
||||
}
|
||||
console.log('socket created', socket)
|
||||
|
||||
const handleMessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
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;
|
||||
}
|
||||
const handler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
socket.addEventListener('message', handler );
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', handleMessage)
|
||||
socket.addEventListener('message', handler );
|
||||
socket.close();
|
||||
};
|
||||
}, [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 (
|
||||
<div className="container draft-panel">
|
||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
||||
<h3>Draft Panel</h3>
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
|
||||
<DraftMoviePool movies={movies}></DraftMoviePool>
|
||||
<ParticipantList
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
isAdmin={false}
|
||||
/>
|
||||
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
|
||||
</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