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