Root cause: Draft list API has NO individual CRUD endpoints. Only endpoints available: - GET (retrieve list) - POST (bulk replacement - deletes all, inserts new list) All modification operations (add, remove, clear, reorder, move) were incorrectly using BaseService CRUD methods (create, delete, patch) which don't exist for this endpoint. Fixes: - add_to_list(): Use bulk replacement with rank insertion logic - remove_player_from_list(): Rebuild list without player - clear_list(): POST empty list - reorder_list(): POST list with new rank order - move_entry_up(): Swap ranks and POST - move_entry_down(): Swap ranks and POST - remove_from_list(): Deprecated (no DELETE endpoint) All operations now: 1. GET current list 2. Build updated list with modifications 3. POST entire updated list (bulk replacement) This resolves all draft list modification failures including: - "Add Failed" when adding players - Remove operations failing silently - Reorder/move operations failing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
497 lines
16 KiB
Python
497 lines
16 KiB
Python
"""
|
|
Draft list service for Discord Bot v2.0
|
|
|
|
Handles team draft list (auto-draft queue) operations. NO CACHING - lists change frequently.
|
|
"""
|
|
import logging
|
|
from typing import Optional, List
|
|
|
|
from services.base_service import BaseService
|
|
from models.draft_list import DraftList
|
|
from exceptions import APIException
|
|
|
|
logger = logging.getLogger(f'{__name__}.DraftListService')
|
|
|
|
|
|
class DraftListService(BaseService[DraftList]):
|
|
"""
|
|
Service for draft list operations.
|
|
|
|
IMPORTANT: This service does NOT use caching decorators because draft lists
|
|
change as users add/remove players from their auto-draft queues.
|
|
|
|
Features:
|
|
- Get team's draft list (ranked by priority)
|
|
- Add player to draft list
|
|
- Remove player from draft list
|
|
- Reorder draft list
|
|
- Clear entire draft list
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize draft list service."""
|
|
super().__init__(DraftList, 'draftlist')
|
|
logger.debug("DraftListService initialized")
|
|
|
|
async def get_team_list(
|
|
self,
|
|
season: int,
|
|
team_id: int
|
|
) -> List[DraftList]:
|
|
"""
|
|
Get team's draft list ordered by rank.
|
|
|
|
NOT cached - teams update their lists frequently during draft.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
|
|
Returns:
|
|
List of DraftList entries ordered by rank (1 = highest priority)
|
|
"""
|
|
try:
|
|
params = [
|
|
('season', str(season)),
|
|
('team_id', str(team_id)),
|
|
('sort', 'rank-asc') # Order by priority
|
|
]
|
|
|
|
entries = await self.get_all_items(params=params)
|
|
logger.debug(f"Found {len(entries)} draft list entries for team {team_id}")
|
|
return entries
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting draft list for team {team_id}: {e}")
|
|
return []
|
|
|
|
async def add_to_list(
|
|
self,
|
|
season: int,
|
|
team_id: int,
|
|
player_id: int,
|
|
rank: Optional[int] = None
|
|
) -> Optional[DraftList]:
|
|
"""
|
|
Add player to team's draft list.
|
|
|
|
If rank is not provided, adds to end of list.
|
|
|
|
NOTE: The API uses bulk replacement - we get the full list, add the new entry,
|
|
and POST the entire updated list back.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
player_id: Player ID to add
|
|
rank: Priority rank (1 = highest), None = add to end
|
|
|
|
Returns:
|
|
Created DraftList entry or None if creation failed
|
|
"""
|
|
try:
|
|
# Get current list
|
|
current_list = await self.get_team_list(season, team_id)
|
|
|
|
# If rank not provided, add to end
|
|
if rank is None:
|
|
rank = len(current_list) + 1
|
|
|
|
# Create new entry data
|
|
new_entry_data = {
|
|
'season': season,
|
|
'team_id': team_id,
|
|
'player_id': player_id,
|
|
'rank': rank
|
|
}
|
|
|
|
# Build complete list for bulk replacement
|
|
draft_list_entries = []
|
|
|
|
# Add existing entries, adjusting ranks if inserting in middle
|
|
for entry in current_list:
|
|
if entry.rank >= rank:
|
|
# Shift down entries at or after insertion point
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': entry.rank + 1
|
|
})
|
|
else:
|
|
# Keep existing rank for entries before insertion point
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': entry.rank
|
|
})
|
|
|
|
# Add new entry
|
|
draft_list_entries.append(new_entry_data)
|
|
|
|
# Sort by rank for consistency
|
|
draft_list_entries.sort(key=lambda x: x['rank'])
|
|
|
|
# POST entire list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': len(draft_list_entries),
|
|
'draft_list': draft_list_entries
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
|
|
# Return the created entry as a DraftList object
|
|
created_entry = DraftList.from_api_data(new_entry_data)
|
|
logger.info(f"Added player {player_id} to team {team_id} draft list at rank {rank}")
|
|
|
|
return created_entry
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding player {player_id} to draft list: {e}")
|
|
return None
|
|
|
|
async def remove_from_list(
|
|
self,
|
|
entry_id: int
|
|
) -> bool:
|
|
"""
|
|
Remove entry from draft list by ID.
|
|
|
|
NOTE: No DELETE endpoint exists. This method is deprecated - use remove_player_from_list() instead.
|
|
|
|
Args:
|
|
entry_id: Draft list entry database ID
|
|
|
|
Returns:
|
|
True if deletion succeeded
|
|
"""
|
|
logger.warning("remove_from_list() called with entry_id - use remove_player_from_list() instead")
|
|
return False
|
|
|
|
async def remove_player_from_list(
|
|
self,
|
|
season: int,
|
|
team_id: int,
|
|
player_id: int
|
|
) -> bool:
|
|
"""
|
|
Remove specific player from team's draft list.
|
|
|
|
Uses bulk replacement pattern - gets full list, removes player, POSTs updated list.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
player_id: Player ID to remove
|
|
|
|
Returns:
|
|
True if player was found and removed
|
|
"""
|
|
try:
|
|
# Get team's list
|
|
current_list = await self.get_team_list(season, team_id)
|
|
|
|
# Check if player is in list
|
|
player_found = any(entry.player_id == player_id for entry in current_list)
|
|
if not player_found:
|
|
logger.warning(f"Player {player_id} not found in team {team_id} draft list")
|
|
return False
|
|
|
|
# Build new list without the player, adjusting ranks
|
|
draft_list_entries = []
|
|
new_rank = 1
|
|
for entry in current_list:
|
|
if entry.player_id != player_id:
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': new_rank
|
|
})
|
|
new_rank += 1
|
|
|
|
# POST updated list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': len(draft_list_entries),
|
|
'draft_list': draft_list_entries
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
logger.info(f"Removed player {player_id} from team {team_id} draft list")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error removing player {player_id} from draft list: {e}")
|
|
return False
|
|
|
|
async def clear_list(
|
|
self,
|
|
season: int,
|
|
team_id: int
|
|
) -> bool:
|
|
"""
|
|
Clear entire draft list for team.
|
|
|
|
Uses bulk replacement pattern - POSTs empty list.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
|
|
Returns:
|
|
True if list was cleared successfully
|
|
"""
|
|
try:
|
|
# Check if list is already empty
|
|
entries = await self.get_team_list(season, team_id)
|
|
if not entries:
|
|
logger.debug(f"No draft list entries to clear for team {team_id}")
|
|
return True
|
|
|
|
entry_count = len(entries)
|
|
|
|
# POST empty list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': 0,
|
|
'draft_list': []
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
logger.info(f"Cleared {entry_count} draft list entries for team {team_id}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error clearing draft list for team {team_id}: {e}")
|
|
return False
|
|
|
|
async def reorder_list(
|
|
self,
|
|
season: int,
|
|
team_id: int,
|
|
new_order: List[int]
|
|
) -> bool:
|
|
"""
|
|
Reorder team's draft list.
|
|
|
|
Uses bulk replacement pattern - builds new list with updated ranks and POSTs it.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
new_order: List of player IDs in desired order
|
|
|
|
Returns:
|
|
True if reordering succeeded
|
|
"""
|
|
try:
|
|
# Get current list
|
|
entries = await self.get_team_list(season, team_id)
|
|
|
|
# Build mapping of player_id -> entry
|
|
entry_map = {e.player_id: e for e in entries}
|
|
|
|
# Build new list in specified order
|
|
draft_list_entries = []
|
|
for new_rank, player_id in enumerate(new_order, start=1):
|
|
if player_id not in entry_map:
|
|
logger.warning(f"Player {player_id} not in draft list, skipping")
|
|
continue
|
|
|
|
entry = entry_map[player_id]
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': new_rank
|
|
})
|
|
|
|
# POST reordered list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': len(draft_list_entries),
|
|
'draft_list': draft_list_entries
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
logger.info(f"Reordered draft list for team {team_id}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reordering draft list for team {team_id}: {e}")
|
|
return False
|
|
|
|
async def move_entry_up(
|
|
self,
|
|
season: int,
|
|
team_id: int,
|
|
player_id: int
|
|
) -> bool:
|
|
"""
|
|
Move player up one position in draft list (higher priority).
|
|
|
|
Uses bulk replacement pattern - swaps ranks and POSTs updated list.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
player_id: Player ID to move up
|
|
|
|
Returns:
|
|
True if move succeeded
|
|
"""
|
|
try:
|
|
entries = await self.get_team_list(season, team_id)
|
|
|
|
# Find player's current position
|
|
current_entry = None
|
|
for entry in entries:
|
|
if entry.player_id == player_id:
|
|
current_entry = entry
|
|
break
|
|
|
|
if not current_entry:
|
|
logger.warning(f"Player {player_id} not found in draft list")
|
|
return False
|
|
|
|
if current_entry.rank == 1:
|
|
logger.debug(f"Player {player_id} already at top of draft list")
|
|
return False
|
|
|
|
# Find entry above (rank - 1)
|
|
above_entry = next((e for e in entries if e.rank == current_entry.rank - 1), None)
|
|
if not above_entry:
|
|
logger.error(f"Could not find entry above rank {current_entry.rank}")
|
|
return False
|
|
|
|
# Build new list with swapped ranks
|
|
draft_list_entries = []
|
|
for entry in entries:
|
|
if entry.player_id == current_entry.player_id:
|
|
# Move this player up
|
|
new_rank = current_entry.rank - 1
|
|
elif entry.player_id == above_entry.player_id:
|
|
# Move above player down
|
|
new_rank = above_entry.rank + 1
|
|
else:
|
|
# Keep existing rank
|
|
new_rank = entry.rank
|
|
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': new_rank
|
|
})
|
|
|
|
# Sort by rank
|
|
draft_list_entries.sort(key=lambda x: x['rank'])
|
|
|
|
# POST updated list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': len(draft_list_entries),
|
|
'draft_list': draft_list_entries
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
logger.info(f"Moved player {player_id} up to rank {current_entry.rank - 1}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error moving player {player_id} up in draft list: {e}")
|
|
return False
|
|
|
|
async def move_entry_down(
|
|
self,
|
|
season: int,
|
|
team_id: int,
|
|
player_id: int
|
|
) -> bool:
|
|
"""
|
|
Move player down one position in draft list (lower priority).
|
|
|
|
Uses bulk replacement pattern - swaps ranks and POSTs updated list.
|
|
|
|
Args:
|
|
season: Draft season
|
|
team_id: Team ID
|
|
player_id: Player ID to move down
|
|
|
|
Returns:
|
|
True if move succeeded
|
|
"""
|
|
try:
|
|
entries = await self.get_team_list(season, team_id)
|
|
|
|
# Find player's current position
|
|
current_entry = None
|
|
for entry in entries:
|
|
if entry.player_id == player_id:
|
|
current_entry = entry
|
|
break
|
|
|
|
if not current_entry:
|
|
logger.warning(f"Player {player_id} not found in draft list")
|
|
return False
|
|
|
|
if current_entry.rank == len(entries):
|
|
logger.debug(f"Player {player_id} already at bottom of draft list")
|
|
return False
|
|
|
|
# Find entry below (rank + 1)
|
|
below_entry = next((e for e in entries if e.rank == current_entry.rank + 1), None)
|
|
if not below_entry:
|
|
logger.error(f"Could not find entry below rank {current_entry.rank}")
|
|
return False
|
|
|
|
# Build new list with swapped ranks
|
|
draft_list_entries = []
|
|
for entry in entries:
|
|
if entry.player_id == current_entry.player_id:
|
|
# Move this player down
|
|
new_rank = current_entry.rank + 1
|
|
elif entry.player_id == below_entry.player_id:
|
|
# Move below player up
|
|
new_rank = below_entry.rank - 1
|
|
else:
|
|
# Keep existing rank
|
|
new_rank = entry.rank
|
|
|
|
draft_list_entries.append({
|
|
'season': entry.season,
|
|
'team_id': entry.team_id,
|
|
'player_id': entry.player_id,
|
|
'rank': new_rank
|
|
})
|
|
|
|
# Sort by rank
|
|
draft_list_entries.sort(key=lambda x: x['rank'])
|
|
|
|
# POST updated list (bulk replacement)
|
|
client = await self.get_client()
|
|
payload = {
|
|
'count': len(draft_list_entries),
|
|
'draft_list': draft_list_entries
|
|
}
|
|
|
|
await client.post(self.endpoint, payload)
|
|
logger.info(f"Moved player {player_id} down to rank {current_entry.rank + 1}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error moving player {player_id} down in draft list: {e}")
|
|
return False
|
|
|
|
|
|
# Global service instance
|
|
draft_list_service = DraftListService()
|