# Refactor DraftState for improved type safety and consistency

- Replaced manual cache key handling with `DraftCache` class using properties
- Fixed connected_participants serialization by converting sets to lists
- Updated countdown clock component to accept unified state prop
This commit is contained in:
2025-08-23 13:55:04 -05:00
parent e8bf313f53
commit b38c779772
2 changed files with 76 additions and 93 deletions

View File

@@ -269,7 +269,7 @@ class DraftParticipantConsumer(DraftConsumerBase):
"subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM, "subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM,
"payload": { "payload": {
"user": self.user.username, "user": self.user.username,
"connected_participants": self.draft_state.connected_participants, "connected_participants": list(self.draft_state.connected_participants),
}, },
}, },
) )
@@ -287,7 +287,7 @@ class DraftParticipantConsumer(DraftConsumerBase):
"subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM, "subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM,
"payload": { "payload": {
"user": self.user.username, "user": self.user.username,
"connected_participants": self.draft_state.connected_participants, "connected_participants": list(self.draft_state.connected_participants),
}, },
}, },
) )

View File

@@ -1,10 +1,10 @@
from django.core.cache import cache from django.core.cache import cache, BaseCache
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from boxofficefantasy.models import Movie from boxofficefantasy.models import Movie
from django.contrib.auth.models import User from django.contrib.auth.models import User
from draft.constants import DraftPhase from draft.constants import DraftPhase
from draft.models import DraftSession from draft.models import DraftSession, DraftSessionSettings
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
@@ -14,114 +14,93 @@ class DraftStateException(Exception):
"""Raised when an action is not allowed due to the current draft state or phase.""" """Raised when an action is not allowed due to the current draft state or phase."""
pass pass
class DraftCacheKeys: class DraftCache:
def __init__(self, id): phase: str
self.prefix = f"draft:{id}" draft_order: str
draft_index: str
current_movie: str
bids: str
bid_timer_start: str
bid_timer_end: str
@property _cached_properties = {
def admins(self): "participants",
return f"{self.prefix}:admins" "phase",
"draft_order",
"draft_index",
"current_movie",
"bids",
"bid_timer_start",
"bid_timer_end",
@property }
def participants(self):
return f"{self.prefix}:participants"
@property
def users(self):
return f"{self.prefix}:users"
@property
def connected_users(self):
return f"{self.prefix}:connected_users"
@property
def phase(self):
return f"{self.prefix}:phase"
@property
def draft_order(self):
return f"{self.prefix}:draft_order"
@property def __init__(self, draft_id: str, cache: BaseCache = cache):
def draft_index(self): super().__setattr__("_cache", self._load_cache(cache))
return f"{self.prefix}:draft_index" super().__setattr__("_prefix", f"draft:{draft_id}:")
@property
def current_movie(self):
return f"{self.prefix}:current_movie"
# @property
# def state(self):
# return f"{self.prefix}:state"
# @property def _load_cache(self, cache) -> BaseCache:
# def current_movie(self): return cache
# return f"{self.prefix}:current_movie"
@property def _save_cache(self) -> None:
def bids(self): # Django cache saves itself
return f"{self.prefix}:bids" return
# @property def __getattr__(self, name: str) -> Any:
# def participants(self): if name == "_prefix": return super().__getattribute__('_prefix')
# return f"{self.prefix}:participants" if name in self._cached_properties:
return self._cache.get(self._prefix+name, None)
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
@property def __setattr__(self, name: str, value: Any):
def bid_timer_end(self): if name in self._cached_properties:
return f"{self.prefix}:bid_timer_end" self._cache.set(self._prefix+name, value)
@property self._save_cache()
def bid_timer_start(self): else:
return f"{self.prefix}:bid_timer_start" super().__setattr__(name, value)
# def user_status(self, user_id): def __delattr__(self, name):
# return f"{self.prefix}:user:{user_id}:status" if name in self._cached_properties:
self._cache.delete(name)
# def user_channel(self, user_id): raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
# return f"{self.prefix}:user:{user_id}:channel"
class DraftStateManager: class DraftStateManager:
def __init__(self, session: DraftSession): def __init__(self, session: DraftSession):
self.session_id = session.hashid self.session_id: str = session.hashid
self.cache = cache self.cache: DraftCache = DraftCache(self.session_id, cache)
self.cache_keys = DraftCacheKeys(self.session_id) self._initial_phase: DraftPhase = self.cache.phase or DraftPhase.WAITING.value
self._initial_phase = self.cache.get(self.cache_keys.phase, DraftPhase.WAITING.value) self.settings: DraftSessionSettings = session.settings
self.settings = session.settings self.participants: set[User] = set(session.participants.all())
self.participants = list(session.participants.all()) self.connected_participants: set[User] = set()
# === Phase Management === # === Phase Management ===
@property @property
def phase(self) -> str: def phase(self) -> str:
return str(self.cache.get(self.cache_keys.phase, self._initial_phase)) return self.cache.phase
@phase.setter @phase.setter
def phase(self, new_phase: DraftPhase): def phase(self, new_phase: DraftPhase) -> None:
self.cache.set(self.cache_keys.phase, new_phase.value) self.cache.phase = new_phase
# === Connected Users === # === Connected Users ===
@property
def connected_participants(self) -> list[str]:
return json.loads(self.cache.get(self.cache_keys.connected_users) or "[]")
def connect_participant(self, username: str): def connect_participant(self, username: str):
users = set(self.connected_participants) self.connected_participants.add(username)
users.add(username) return self.connected_participants
self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
def disconnect_participant(self, username: str): def disconnect_participant(self, username: str):
users = set(self.connected_participants) self.connected_participants.discard(username)
users.discard(username)
self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
# === Draft Order === # === Draft Order ===
@property @property
def draft_order(self): def draft_order(self):
return json.loads(self.cache.get(self.cache_keys.draft_order,"[]")) return json.loads(self.cache.draft_order or "[]")
@draft_order.setter @draft_order.setter
def draft_order(self, draft_order: list[str]): def draft_order(self, draft_order: list[str]):
if not isinstance(draft_order, list): if not isinstance(draft_order, list):
return return
self.cache.set(self.cache_keys.draft_order,json.dumps(draft_order)) self.cache.draft_order = json.dumps(draft_order)
def determine_draft_order(self) -> List[User]: def determine_draft_order(self) -> List[User]:
self.phase = DraftPhase.DETERMINE_ORDER self.phase = DraftPhase.DETERMINE_ORDER
@@ -134,11 +113,15 @@ class DraftStateManager:
@property @property
def draft_index(self): def draft_index(self):
return self.cache.get(self.cache_keys.draft_index,0) draft_index = self.cache.draft_index
if not draft_index:
draft_index = 0
self.cache.draft_index = draft_index
return self.cache.draft_index
@draft_index.setter @draft_index.setter
def draft_index(self, draft_index: int): def draft_index(self, draft_index: int):
self.cache.set(self.cache_keys.draft_index, int(draft_index)) self.cache.draft_index = draft_index
def draft_index_advance(self, n: int = 1): def draft_index_advance(self, n: int = 1):
self.draft_index += n self.draft_index += n
@@ -178,21 +161,21 @@ class DraftStateManager:
# === Current Nomination / Bid === # === Current Nomination / Bid ===
def start_nomination(self, movie_id: int): def start_nomination(self, movie_id: int):
self.cache.set(self.cache_keys.current_movie, movie_id) self.cache.current_movie = movie_id
self.cache.delete(self.cache_keys.bids) self.cache.bids = []
def place_bid(self, user: User, amount: int|str): def place_bid(self, user: User, amount: int|str):
if isinstance(amount, str): if isinstance(amount, str):
amount = int(amount) amount = int(amount)
bids = self.get_bids() bids = self.get_bids()
bids.append({"user":user.username, "amount":amount}) bids.append({"user":user.username, "amount":amount})
self.cache.set(self.cache_keys.bids, json.dumps(bids)) self.cache.bids = json.dumps(bids)
def get_bids(self) -> dict: def get_bids(self) -> dict:
return json.loads(self.cache.get(self.cache_keys.bids) or "[]") return json.loads(self.cache.bids or "[]")
def current_movie(self) -> Movie | None: def current_movie(self) -> Movie | None:
movie_id = self.cache.get(self.cache_keys.current_movie) movie_id = self.current_movie
return Movie.objects.filter(pk=movie_id).first() if movie_id else None return Movie.objects.filter(pk=movie_id).first() if movie_id else None
def start_bidding(self): def start_bidding(self):
@@ -200,14 +183,14 @@ class DraftStateManager:
seconds = self.settings.bidding_duration seconds = self.settings.bidding_duration
start_time = time.time() start_time = time.time()
end_time = start_time + seconds end_time = start_time + seconds
self.cache.set(self.cache_keys.bid_timer_end, end_time) self.cache.bid_timer_end = end_time
self.cache.set(self.cache_keys.bid_timer_start, start_time) self.cache.bid_timer_start = start_time
def get_timer_end(self) -> str | None: def get_timer_end(self) -> str | None:
return self.cache.get(self.cache_keys.bid_timer_end) return self.cache.bid_timer_end
def get_timer_start(self) -> str | None: def get_timer_start(self) -> str | None:
return self.cache.get(self.cache_keys.bid_timer_start) return self.cache.bid_timer_start
# === Sync Snapshot === # === Sync Snapshot ===
def to_dict(self) -> dict: def to_dict(self) -> dict:
@@ -216,8 +199,8 @@ class DraftStateManager:
"phase": self.phase, "phase": self.phase,
"draft_order": self.draft_order, "draft_order": self.draft_order,
"draft_index": self.draft_index, "draft_index": self.draft_index,
"connected_participants": self.connected_participants, "connected_participants": list(self.connected_participants),
"current_movie": self.cache.get(self.cache_keys.current_movie), "current_movie": self.cache.current_movie,
"bids": self.get_bids(), "bids": self.get_bids(),
"bidding_timer_end": self.get_timer_end(), "bidding_timer_end": self.get_timer_end(),
"bidding_timer_start": self.get_timer_start(), "bidding_timer_start": self.get_timer_start(),