Integrate draft session support with phase handling and real-time updates
- Added user authentication UI in the base template for navbar. - Expanded `league.dj.html` to include a new "Draft Sessions" tab showing active drafts. - Refactored Django views and models to support `DraftSession` with participants and movies. - Replaced deprecated models like `DraftParticipant` and `DraftMoviePool` with a new schema using `DraftSessionParticipant`. - Introduced WebSocket consumers (`DraftAdminConsumer`, `DraftParticipantConsumer`) with structured phase logic and caching. - Added `DraftStateManager` for managing draft state in Django cache. - Created frontend UI components in React for draft admin and participants, including phase control and WebSocket message logging. - Updated SCSS styles for improved UI structure and messaging area.
This commit is contained in:
@@ -1,20 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from draft.models import DraftSession, DraftParticipant, DraftMoviePool, DraftPick, DraftSessionSettings
|
||||
from draft.models import DraftSession, DraftPick, DraftSessionSettings, DraftSessionParticipant
|
||||
from boxofficefantasy.models import User
|
||||
|
||||
class DraftSessionSettingsInline(admin.TabularInline): # or TabularInline
|
||||
model = DraftSessionSettings
|
||||
can_delete = False
|
||||
show_change_link = True
|
||||
class DraftParticipantInline(admin.TabularInline):
|
||||
class DrafteSessionUserInline(admin.TabularInline):
|
||||
extra = 0
|
||||
model = DraftParticipant
|
||||
model = DraftSessionParticipant
|
||||
class DraftSessionAdmin(admin.ModelAdmin):
|
||||
inlines = [DraftSessionSettingsInline, DraftParticipantInline]
|
||||
readonly_fields = ('hashed_id',)
|
||||
inlines = [DraftSessionSettingsInline, DrafteSessionUserInline]
|
||||
readonly_fields = ('hashid',)
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(DraftSession, DraftSessionAdmin)
|
||||
admin.site.register(DraftSessionSettings)
|
||||
admin.site.register(DraftParticipant)
|
||||
admin.site.register(DraftMoviePool)
|
||||
admin.site.register(DraftPick)
|
||||
|
||||
@@ -3,11 +3,11 @@ from enum import IntEnum
|
||||
class DraftMessage:
|
||||
# Server
|
||||
INFORM_PHASE_CHANGE = "inform.phase.change"
|
||||
CONFIRM_PHASE_ADVANCE = "confirm.phase.advance"
|
||||
INFORM_STATUS = "inform.status"
|
||||
CONFIRM_PHASE_CHANGE = "confirm.phase.change"
|
||||
INFORM_PHASE = "inform.phase"
|
||||
|
||||
# Client
|
||||
REQUEST_PHASE_ADVANCE = "request.phase.advance"
|
||||
REQUEST_PHASE_CHANGE = "request.phase.change"
|
||||
REQUEST_INFORM_STATUS = "request.inform.status"
|
||||
|
||||
# Waiting Phase
|
||||
@@ -15,10 +15,12 @@ class DraftMessage:
|
||||
INFORM_JOIN_USER = "inform.join.user"
|
||||
REQUEST_JOIN_PARTICIPANT = "request.join.participant"
|
||||
REQUEST_JOIN_ADMIN = "request.join.admin"
|
||||
INFORM_LEAVE_PARTICIPANT = "inform.leave.participant"
|
||||
|
||||
## 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"
|
||||
|
||||
# Determine Order
|
||||
@@ -54,42 +56,4 @@ class DraftGroupChannelNames:
|
||||
@property
|
||||
def participant(self):
|
||||
return f"{self.prefix}.participant"
|
||||
|
||||
class DraftCacheKeys:
|
||||
def __init__(self, id):
|
||||
self.prefix = f"draft:{id}"
|
||||
|
||||
@property
|
||||
def admins(self):
|
||||
return f"{self.prefix}:admins"
|
||||
|
||||
@property
|
||||
def participants(self):
|
||||
return f"{self.prefix}:participants"
|
||||
|
||||
|
||||
# @property
|
||||
# def state(self):
|
||||
# return f"{self.prefix}:state"
|
||||
|
||||
# @property
|
||||
# def current_movie(self):
|
||||
# return f"{self.prefix}:current_movie"
|
||||
|
||||
# @property
|
||||
# def bids(self):
|
||||
# return f"{self.prefix}:bids"
|
||||
|
||||
# @property
|
||||
# def participants(self):
|
||||
# return f"{self.prefix}:participants"
|
||||
|
||||
# @property
|
||||
# def bid_timer_end(self):
|
||||
# return f"{self.prefix}:bid_timer_end"
|
||||
|
||||
# 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"
|
||||
|
||||
@@ -3,61 +3,125 @@ from channels.db import database_sync_to_async
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from boxofficefantasy.models import League, Season
|
||||
from boxofficefantasy.views import parse_season_slug
|
||||
from draft.models import DraftSession, DraftPick, DraftMoviePool, DraftParticipant
|
||||
from draft.models import DraftSession, DraftSessionParticipant
|
||||
from django.core.cache import cache
|
||||
from draft.constants import DraftMessage, DraftPhase, DraftGroupChannelNames
|
||||
import asyncio
|
||||
from django.contrib.auth.models import User
|
||||
from draft.constants import (
|
||||
DraftMessage,
|
||||
DraftPhase,
|
||||
DraftGroupChannelNames,
|
||||
)
|
||||
from draft.state import DraftCacheKeys, DraftStateManager
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
group_names: DraftGroupChannelNames
|
||||
cache_keys: DraftCacheKeys
|
||||
draft_state: DraftStateManager
|
||||
user: User
|
||||
|
||||
async def connect(self):
|
||||
draft_session_id_hashed = self.scope["url_route"]["kwargs"].get(
|
||||
"draft_session_id_hashed"
|
||||
)
|
||||
league_slug = self.scope["url_route"]["kwargs"].get("league_slug")
|
||||
season_slug = self.scope["url_route"]["kwargs"].get("season_slug")
|
||||
draft_hashid = self.scope["url_route"]["kwargs"].get("draft_session_id_hashed")
|
||||
|
||||
self.draft_session = await self.get_draft_session(
|
||||
draft_session_id_hashed=draft_session_id_hashed,
|
||||
draft_session_id_hashed=draft_hashid,
|
||||
)
|
||||
self.draft_participants = await self.get_draft_participants(
|
||||
session=self.draft_session
|
||||
)
|
||||
|
||||
self.draft_group_names = f"draft_admin_{self.draft_session.hashed_id}"
|
||||
self.draft_participant_group_channels = DraftGroupChannelNames(draft_session_id_hashed)
|
||||
self.group_names = DraftGroupChannelNames(draft_hashid)
|
||||
self.cache_keys = DraftCacheKeys(draft_hashid)
|
||||
self.draft_state = DraftStateManager(draft_hashid)
|
||||
|
||||
self.user = self.scope["user"]
|
||||
if not self.user.is_authenticated:
|
||||
if not self.should_accept_user():
|
||||
await self.send_json({
|
||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
||||
"user": self.user.username
|
||||
})
|
||||
await self.close()
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.REJECT_JOIN_PARTICIPANT,
|
||||
"user": self.user.username
|
||||
},
|
||||
)
|
||||
return
|
||||
else:
|
||||
await self.accept()
|
||||
await self.channel_layer.group_add(
|
||||
self.group_names.session, self.channel_name
|
||||
)
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.INFORM_JOIN_USER,
|
||||
"user": self.user.username
|
||||
},
|
||||
)
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.INFORM_PHASE,
|
||||
"phase": str(self.draft_state.phase)
|
||||
}
|
||||
)
|
||||
|
||||
async def should_accept_user(self)->bool:
|
||||
return self.user.is_authenticated
|
||||
|
||||
async def receive_json(self, content):
|
||||
event_type = content.get("type")
|
||||
user = self.scope["user"]
|
||||
|
||||
async def user_joined(self, event):
|
||||
async def inform_leave_participant(self,event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "user.joined",
|
||||
"user": event["user"].username,
|
||||
"user_type": event["user_type"],
|
||||
"users": event["users"],
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
"participants": [user.username for user in self.draft_participants],
|
||||
"connected_participants": self.draft_state.connected_users
|
||||
}
|
||||
)
|
||||
|
||||
async def send_draft_summary(self):
|
||||
state = cache.get(self.draft_status_cache_key, {})
|
||||
async def inform_join_user(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "draft_summary",
|
||||
"phase": state.get("phase", "not started"),
|
||||
"movie": state.get("movie"),
|
||||
"current_bid": state.get("current_bid"),
|
||||
"time_remaining": state.get("time_remaining"),
|
||||
"you_are_next": state.get("you_are_next", False),
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
"participants": [user.username for user in self.draft_participants],
|
||||
"connected_participants": self.draft_state.connected_users
|
||||
}
|
||||
)
|
||||
|
||||
async def reject_join_participant(self,event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
"participants": [user.username for user in self.draft_participants],
|
||||
"connected_participants": self.draft_state.connected_users
|
||||
}
|
||||
)
|
||||
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 send_draft_summary(self): ...
|
||||
|
||||
# === Broadcast handlers ===
|
||||
async def draft_status(self, event):
|
||||
await self.send_json(
|
||||
@@ -69,36 +133,20 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
|
||||
# === DB Access ===
|
||||
@database_sync_to_async
|
||||
def get_draft_session(
|
||||
self, draft_session_id_hashed, league_slug, season_slug
|
||||
) -> DraftSession:
|
||||
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
||||
draft_session_id = DraftSession.decode_id(draft_session_id_hashed)
|
||||
if draft_session_id:
|
||||
draft_session = DraftSession.objects.select_related(
|
||||
"season", "season__league"
|
||||
).get(pk=draft_session_id)
|
||||
elif league_slug and season_slug:
|
||||
label, year = parse_season_slug(season_slug)
|
||||
season = Season.objects.filter(label=label, year=year).first()
|
||||
draft_session = (
|
||||
DraftSession.objects.select_related("season", "season__league")
|
||||
.filter(season=season)
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
raise Exception()
|
||||
|
||||
return draft_session
|
||||
|
||||
@database_sync_to_async
|
||||
def get_draft_participants(self) -> list[DraftParticipant]:
|
||||
# Replace this with real queryset to fetch users in draft
|
||||
participants = DraftParticipant.objects.select_related("user").filter(
|
||||
draft=self.draft_session
|
||||
)
|
||||
connected_ids = cache.get(self.draft_connected_participants_cache_key, set())
|
||||
for p in participants:
|
||||
p.is_connected = p in connected_ids
|
||||
def get_draft_participants(self, session) -> list[DraftSessionParticipant]:
|
||||
participants = session.participants.all()
|
||||
return list(participants.all())
|
||||
|
||||
|
||||
@@ -109,192 +157,118 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
await self.close()
|
||||
return
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.draft_admin_group_name, self.channel_name
|
||||
await self.channel_layer.group_add(self.group_names.admin, self.channel_name)
|
||||
|
||||
async def receive_json(self, content):
|
||||
await super().receive_json(content)
|
||||
event_type = content.get("type")
|
||||
user = self.scope["user"]
|
||||
destination = DraftPhase(content.get("destination"))
|
||||
if (
|
||||
event_type == DraftMessage.REQUEST_PHASE_CHANGE
|
||||
and destination == DraftPhase.DETERMINE_ORDER
|
||||
):
|
||||
await self.determine_draft_order()
|
||||
|
||||
def should_accept_user(self):
|
||||
return super().should_accept_user() and self.user.is_staff
|
||||
|
||||
# === Draft logic ===
|
||||
async def determine_draft_order(self):
|
||||
draft_order = random.sample(self.draft_participants, len(self.draft_participants))
|
||||
self.draft_state.draft_order = [p.username for p in draft_order]
|
||||
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.CONFIRM_DETERMINE_DRAFT_ORDER,
|
||||
"payload": {
|
||||
"draft_order": self.draft_state.draft_order
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# await self.channel_layer.group_send(
|
||||
# self.draft_participant_group_name,
|
||||
# {"type": "user.joined", "user": self.user, "user_type": "admin"},
|
||||
# )
|
||||
|
||||
async def set_draft_phase(self, destination: DraftPhase):
|
||||
self.draft_state.phase = destination
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_admin_group_name,
|
||||
{"type": "user.joined", "user": self.user, "user_type": "admin"},
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.CONFIRM_PHASE_CHANGE,
|
||||
"payload": {
|
||||
"phase": self.draft_state.phase
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# === Broadcast Handlers ===
|
||||
|
||||
async def confirm_phase_change(self, event):
|
||||
await self.send_json({
|
||||
"type": event["type"],
|
||||
"payload": event["payload"]
|
||||
})
|
||||
|
||||
class DraftParticipantConsumer(DraftConsumerBase):
|
||||
async def connect(self):
|
||||
await super().connect()
|
||||
|
||||
self.draft_state.connect_user(self.user.username)
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.group_names.participant, self.channel_name
|
||||
)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": DraftMessage.INFORM_LEAVE_PARTICIPANT,
|
||||
"user": self.user.username
|
||||
},
|
||||
)
|
||||
await super().disconnect(close_code)
|
||||
self.draft_state.disconnect_user(self.user.username)
|
||||
await self.channel_layer.group_discard(
|
||||
self.group_names.session, self.channel_name
|
||||
)
|
||||
|
||||
def should_accept_user(self):
|
||||
return super().should_accept_user() and self.user in self.draft_participants
|
||||
|
||||
async def receive_json(self, content):
|
||||
await super().receive_json(content)
|
||||
event_type = content.get("type")
|
||||
user = self.scope["user"]
|
||||
|
||||
if event_type == "start.draft":
|
||||
await self.start_draft()
|
||||
elif event_type == "user.joined":
|
||||
pass
|
||||
elif event_type == "nominate":
|
||||
await self.nominate(content.get("movie"))
|
||||
elif event_type == "bid":
|
||||
await self.place_bid(content.get("amount"), self.scope["user"].username)
|
||||
elif event_type == "message":
|
||||
if event_type == DraftMessage.REQUEST_JOIN_PARTICIPANT:
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_participant_group_name,
|
||||
{
|
||||
"type": "chat.message",
|
||||
"user": self.scope["user"].username,
|
||||
"message": content.get("message"),
|
||||
},
|
||||
)
|
||||
|
||||
# === Draft logic (stubbed for now) ===
|
||||
|
||||
async def start_draft(self):
|
||||
# Example: shuffle draft order
|
||||
participants = await self.get_draft_participants()
|
||||
draft_order = random.sample(participants, len(participants))
|
||||
connected_participants = cache.get(
|
||||
self.draft_connected_participants_cache_key, ()
|
||||
)
|
||||
|
||||
initial_state = {
|
||||
"phase": "nominating",
|
||||
"current_nominee": None,
|
||||
"current_bid": None,
|
||||
"participants": [
|
||||
{"user": p.user.username, "is_connected": p in connected_participants}
|
||||
for p in await self.get_draft_participants()
|
||||
],
|
||||
"draft_order": [p.user.username for p in draft_order],
|
||||
"current_turn_index": 0,
|
||||
"picks": [],
|
||||
}
|
||||
cache.set(self.draft_status_cache_key, initial_state)
|
||||
|
||||
for group_name in [
|
||||
self.draft_admin_group_name,
|
||||
self.draft_participant_group_name,
|
||||
]:
|
||||
# await self.channel_layer.group_send(
|
||||
# group_name,
|
||||
# {
|
||||
# "type": "draft.start"
|
||||
# }
|
||||
# )
|
||||
await self.channel_layer.group_send(
|
||||
group_name,
|
||||
{
|
||||
"type": "draft.status",
|
||||
"status": cache.get(self.draft_status_cache_key),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class DraftParticipantConsumer(DraftConsumerBase):
|
||||
async def connect(self):
|
||||
await super().connect()
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.draft_participant_group_name, self.channel_name
|
||||
)
|
||||
|
||||
try:
|
||||
await self.add_draft_participant()
|
||||
except Exception as e:
|
||||
await self.close()
|
||||
return
|
||||
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "connection.accepted",
|
||||
"user": self.user.username,
|
||||
"is_staff": self.user.is_staff,
|
||||
}
|
||||
)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_participant_group_name,
|
||||
{
|
||||
"type": "user.joined",
|
||||
"user": self.user,
|
||||
"user_type": "participant",
|
||||
"participants": [],
|
||||
},
|
||||
)
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_admin_group_name,
|
||||
{"type": "user.joined", "user": self.user, "user_type": "participant"},
|
||||
)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_discard(
|
||||
self.draft_participant_group_name, self.channel_name
|
||||
)
|
||||
|
||||
async def receive_json(self, content):
|
||||
event_type = content.get("type")
|
||||
user = self.scope["user"]
|
||||
|
||||
if event_type == "user.joined":
|
||||
pass
|
||||
elif event_type == "nominate":
|
||||
await self.nominate(content.get("movie"))
|
||||
elif event_type == "bid":
|
||||
await self.place_bid(content.get("amount"), self.scope["user"].username)
|
||||
elif event_type == "message":
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_participant_group_name,
|
||||
{
|
||||
"type": "chat.message",
|
||||
"user": self.scope["user"].username,
|
||||
"message": content.get("message"),
|
||||
},
|
||||
self.group_names.admin,
|
||||
{"type": DraftMessage.REQUEST_JOIN_PARTICIPANT, "user": user},
|
||||
)
|
||||
|
||||
# === Broadcast handlers ===
|
||||
|
||||
async def chat_message(self, event):
|
||||
async def request_join_participant(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "chat.message",
|
||||
"type": event["type"],
|
||||
"user": event["user"],
|
||||
}
|
||||
)
|
||||
|
||||
async def draft_update(self, event):
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "draft.update",
|
||||
"state": event["state"],
|
||||
}
|
||||
)
|
||||
# === Draft ===
|
||||
|
||||
# === Draft logic (stubbed for now) ===
|
||||
async def nominate(self, movie_title): ...
|
||||
|
||||
async def nominate(self, movie_title):
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_participant_group_name,
|
||||
{
|
||||
"type": "draft.update",
|
||||
"state": {
|
||||
"status": "nominating",
|
||||
"movie": movie_title,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def place_bid(self, amount, user):
|
||||
await self.channel_layer.group_send(
|
||||
self.draft_participant_group_name,
|
||||
{
|
||||
"type": "draft.update",
|
||||
"state": {"status": "bidding", "bid": {"amount": amount, "user": user}},
|
||||
},
|
||||
)
|
||||
async def place_bid(self, amount, user): ...
|
||||
|
||||
# === Example DB Access ===
|
||||
|
||||
@database_sync_to_async
|
||||
def add_draft_participant(self):
|
||||
self.participant, _ = DraftParticipant.objects.get_or_create(
|
||||
self.participant, _ = DraftSessionParticipant.objects.get_or_create(
|
||||
user=self.user,
|
||||
draft=self.draft_session,
|
||||
defaults={"budget": self.draft_session.settings.starting_budget},
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-02 00:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boxofficefantasy', '0009_alter_moviemetric_value_alter_pick_bid_amount_and_more'),
|
||||
('draft', '0005_remove_draftsession_settings_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='draftparticipant',
|
||||
name='draft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='draftparticipant',
|
||||
name='user',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='draftsessionsettings',
|
||||
options={'verbose_name_plural': 'Draft session settings'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='draftsession',
|
||||
name='current_nomination_index',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='draftsession',
|
||||
name='is_active',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='draftsession',
|
||||
name='movies',
|
||||
field=models.ManyToManyField(related_name='draft_sessions', to='boxofficefantasy.movie'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DraftSessionParticipant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('draft_session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='draft.draftsession')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('draft_session', 'user')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='draftsession',
|
||||
name='participants',
|
||||
field=models.ManyToManyField(related_name='participant_entries', through='draft.DraftSessionParticipant', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DraftMoviePool',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DraftParticipant',
|
||||
),
|
||||
]
|
||||
@@ -1,38 +1,53 @@
|
||||
from django.db.models import ForeignKey, Model, IntegerField, BooleanField, CASCADE, PROTECT, OneToOneField
|
||||
from django.db.models import (
|
||||
ForeignKey,
|
||||
Model,
|
||||
IntegerField,
|
||||
BooleanField,
|
||||
CASCADE,
|
||||
PROTECT,
|
||||
OneToOneField,
|
||||
ManyToManyField,
|
||||
)
|
||||
from boxofficefantasy.models import Season, User, Movie
|
||||
from boxofficefantasy_project.utils import encode_id, decode_id
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class DraftSession(Model):
|
||||
season = ForeignKey(Season, on_delete=CASCADE)
|
||||
is_active = BooleanField()
|
||||
current_nomination_index = IntegerField()
|
||||
|
||||
participants: ManyToManyField = ManyToManyField(
|
||||
User, through="DraftSessionParticipant", related_name="participant_entries"
|
||||
)
|
||||
movies: ManyToManyField = ManyToManyField(Movie, related_name="draft_sessions", blank=True)
|
||||
|
||||
@property
|
||||
def hashed_id(self):
|
||||
if not self.pk: return ""
|
||||
def hashid(self):
|
||||
if not self.pk:
|
||||
return ""
|
||||
return f"{encode_id(self.pk)}"
|
||||
|
||||
|
||||
@classmethod
|
||||
def decode_id(cls, hashed_id:str) -> id:
|
||||
def decode_id(cls, hashed_id: str) -> id:
|
||||
return decode_id(hashed_id)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
if is_new and not hasattr(self, 'settings'):
|
||||
if is_new and not hasattr(self, "settings"):
|
||||
DraftSessionSettings.objects.create(draft_session=self)
|
||||
|
||||
|
||||
class DraftParticipant(Model):
|
||||
draft = ForeignKey(DraftSession, on_delete=CASCADE)
|
||||
class DraftSessionParticipant(Model):
|
||||
draft_session = ForeignKey(DraftSession, on_delete=CASCADE, blank=True)
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
budget = IntegerField()
|
||||
|
||||
class DraftMoviePool(Model):
|
||||
draft = ForeignKey(DraftSession, on_delete=CASCADE)
|
||||
movie = ForeignKey(Movie, on_delete=CASCADE)
|
||||
nominated = BooleanField()
|
||||
class Meta:
|
||||
unique_together = [("draft_session", "user")]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} in {self.draft_session}"
|
||||
|
||||
|
||||
class DraftPick(Model):
|
||||
draft = ForeignKey(DraftSession, on_delete=CASCADE)
|
||||
@@ -41,16 +56,15 @@ class DraftPick(Model):
|
||||
bid_amount = IntegerField()
|
||||
nomination_order = IntegerField()
|
||||
|
||||
|
||||
class DraftSessionSettings(Model):
|
||||
starting_budget = IntegerField(default=100)
|
||||
draft_session = OneToOneField(
|
||||
DraftSession,
|
||||
on_delete=CASCADE,
|
||||
related_name="settings"
|
||||
DraftSession, on_delete=CASCADE, related_name="settings"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Settings for {self.draft_session}"
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Draft session settings"
|
||||
verbose_name_plural = "Draft session settings"
|
||||
|
||||
137
draft/state.py
Normal file
137
draft/state.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from django.core.cache import cache
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from boxofficefantasy.models import Movie
|
||||
from django.contrib.auth.models import User
|
||||
from draft.constants import DraftPhase
|
||||
|
||||
class DraftCacheKeys:
|
||||
def __init__(self, id):
|
||||
self.prefix = f"draft:{id}"
|
||||
|
||||
@property
|
||||
def admins(self):
|
||||
return f"{self.prefix}:admins"
|
||||
|
||||
@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 state(self):
|
||||
# return f"{self.prefix}:state"
|
||||
|
||||
# @property
|
||||
# def current_movie(self):
|
||||
# return f"{self.prefix}:current_movie"
|
||||
|
||||
# @property
|
||||
# def bids(self):
|
||||
# return f"{self.prefix}:bids"
|
||||
|
||||
# @property
|
||||
# def participants(self):
|
||||
# return f"{self.prefix}:participants"
|
||||
|
||||
# @property
|
||||
# def bid_timer_end(self):
|
||||
# return f"{self.prefix}:bid_timer_end"
|
||||
|
||||
# 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"
|
||||
|
||||
class DraftStateManager:
|
||||
def __init__(self, session_id: int):
|
||||
self.session_id = session_id
|
||||
self.cache = cache
|
||||
self.keys = DraftCacheKeys(session_id)
|
||||
self._phase = self.cache.get(self.keys.phase, DraftPhase.WAITING)
|
||||
self.draft_order = self.cache.get(self.keys.draft_order)
|
||||
|
||||
|
||||
# === Phase Management ===
|
||||
@property
|
||||
def phase(self) -> str:
|
||||
return str(self.cache.get(self.keys.phase, self._phase))
|
||||
|
||||
@phase.setter
|
||||
def phase(self, new_phase: DraftPhase):
|
||||
self.cache.set(self.keys.phase, new_phase)
|
||||
|
||||
# === Connected Users ===
|
||||
@property
|
||||
def connected_users(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)
|
||||
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)
|
||||
users.discard(username)
|
||||
self.cache.set(self.keys.connected_users, json.dumps(list(users)))
|
||||
|
||||
# === Draft Order ===
|
||||
@property
|
||||
def draft_order(self):
|
||||
return json.loads(self.cache.get(self.keys.draft_order,"[]"))
|
||||
|
||||
@draft_order.setter
|
||||
def draft_order(self, draft_order: list[User]):
|
||||
self.cache.set(self.keys.draft_order,json.dumps(draft_order))
|
||||
|
||||
# === Current Nomination / Bid ===
|
||||
def start_nomination(self, movie_id: int):
|
||||
self.cache.set(self.keys.current_movie, movie_id)
|
||||
self.cache.delete(self.keys.bids)
|
||||
|
||||
def place_bid(self, user_id: int, amount: int):
|
||||
bids = self.get_bids()
|
||||
bids[user_id] = amount
|
||||
self.cache.set(self.keys.bids, json.dumps(bids))
|
||||
|
||||
def get_bids(self) -> dict:
|
||||
return json.loads(self.cache.get(self.keys.bids) or "{}")
|
||||
|
||||
def current_movie(self) -> Movie | None:
|
||||
movie_id = self.cache.get(self.keys.current_movie)
|
||||
return Movie.objects.filter(pk=movie_id).first() if movie_id else None
|
||||
|
||||
def start_timer(self, seconds: int):
|
||||
end_time = (datetime.now() + timedelta(seconds=seconds)).isoformat()
|
||||
self.cache.set(self.keys.timer_end, end_time)
|
||||
self.cache.set(self.keys.timer_end, end_time)
|
||||
|
||||
def get_timer_end(self) -> str | None:
|
||||
return self.cache.get(self.keys.timer_end).decode("utf-8") if self.cache.get(self.keys.timer_end) else None
|
||||
|
||||
# === Sync Snapshot ===
|
||||
def get_summary(self) -> dict:
|
||||
return {
|
||||
"phase": self.phase,
|
||||
"connected_users": self.connected_users,
|
||||
"current_movie": self.cache.get(self.keys.current_movie),
|
||||
"bids": self.get_bids(),
|
||||
"timer_end": self.get_timer_end(),
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{% extends "base.dj.html" %}
|
||||
{% block content %}
|
||||
<h1>Draft Room: {{ league.name }} – {{ season.label }} {{ season.year }}</h1>
|
||||
{% load static %}
|
||||
<div id="draft-app" data-draft-id="{{draft_id_hashed}}"></div>
|
||||
@@ -5,4 +7,5 @@
|
||||
<script src="http://localhost:3000/dist/bundle.js"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'bundle.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,5 @@
|
||||
{% extends "base.dj.html" %}
|
||||
{% block content %}
|
||||
<h1>Draft Room: {{ league.name }} – {{ season.label }} {{ season.year }}</h1>
|
||||
{% load static %}
|
||||
<div id="draft-admin-app" data-draft-id="{{ draft_id_hashed }}"></div>
|
||||
@@ -6,3 +8,4 @@
|
||||
{% else %}
|
||||
<script src="{% static 'bundle.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -20,7 +20,7 @@ def draft_room(request, league_slug=None, season_slug=None, draft_session_id_has
|
||||
draft_session = get_object_or_404(DraftSession, season=season)
|
||||
|
||||
context = {
|
||||
"draft_id_hashed": draft_session.hashed_id,
|
||||
"draft_id_hashed": draft_session.hashid,
|
||||
"league": league,
|
||||
"season": season,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user