Root Cause Fixes: - Add _extract_items_and_count_from_response() override to DraftPickService to handle API returning 'picks' key instead of 'draftpicks' - Add custom from_api_data() to DraftPick model to handle API field mapping (origowner/owner/player -> origowner_id/owner_id/player_id) Enhancements: - Add timer status to /draft-admin set-pick success message - Shows relative deadline timestamp when timer active - Shows "Timer Inactive" when timer not running Also includes related draft module improvements from prior work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
245 lines
9.2 KiB
Python
245 lines
9.2 KiB
Python
"""
|
|
Team model for SBA teams
|
|
|
|
Represents a team in the league with all associated metadata.
|
|
"""
|
|
from typing import Optional
|
|
from enum import Enum
|
|
from pydantic import Field
|
|
|
|
from config import get_config
|
|
from models.base import SBABaseModel
|
|
from models.division import Division
|
|
from models.manager import Manager
|
|
|
|
|
|
class RosterType(Enum):
|
|
"""Roster designation types."""
|
|
MAJOR_LEAGUE = "ml"
|
|
MINOR_LEAGUE = "mil"
|
|
INJURED_LIST = "il"
|
|
FREE_AGENCY = "fa"
|
|
|
|
|
|
class Team(SBABaseModel):
|
|
"""Team model representing an SBA team."""
|
|
|
|
# Override base model to make id required for database entities
|
|
id: int = Field(..., description="Team ID from database")
|
|
|
|
abbrev: str = Field(..., description="Team abbreviation (e.g., 'NYY')")
|
|
sname: str = Field(..., description="Short team name")
|
|
lname: str = Field(..., description="Long team name")
|
|
season: int = Field(..., description="Season number")
|
|
|
|
# Manager information
|
|
gmid: Optional[int] = Field(None, description="Primary general manager ID")
|
|
gmid2: Optional[int] = Field(None, description="Secondary general manager ID")
|
|
manager1_id: Optional[int] = Field(None, description="Primary manager ID")
|
|
manager1: Optional[Manager] = Field(None, description="Manager object")
|
|
manager2_id: Optional[int] = Field(None, description="Secondary manager ID")
|
|
manager2: Optional[Manager] = Field(None, description="Manager object")
|
|
|
|
# Team metadata
|
|
division_id: Optional[int] = Field(None, description="Division ID")
|
|
division: Optional[Division] = Field(None, description="Division object (populated from API)")
|
|
stadium: Optional[str] = Field(None, description="Home stadium name")
|
|
thumbnail: Optional[str] = Field(None, description="Team thumbnail URL")
|
|
color: Optional[str] = Field(None, description="Primary team color")
|
|
dice_color: Optional[str] = Field(None, description="Dice rolling color")
|
|
salary_cap: Optional[float] = Field(None, description="Team-specific salary cap (None uses default)")
|
|
|
|
@classmethod
|
|
def from_api_data(cls, data: dict) -> 'Team':
|
|
"""
|
|
Create Team instance from API data, handling nested division structure.
|
|
|
|
The API returns division data as a nested object, but our model expects
|
|
both division_id (int) and division (optional Division object).
|
|
"""
|
|
# Make a copy to avoid modifying original data
|
|
team_data = data.copy()
|
|
|
|
# Handle nested division structure
|
|
if 'division' in team_data and isinstance(team_data['division'], dict):
|
|
division_data = team_data['division']
|
|
# Extract division_id from nested division object
|
|
team_data['division_id'] = division_data.get('id')
|
|
# Keep division object for optional population
|
|
if division_data.get('id'):
|
|
team_data['division'] = Division.from_api_data(division_data)
|
|
|
|
return super().from_api_data(team_data)
|
|
|
|
def roster_type(self) -> RosterType:
|
|
"""
|
|
Determine the roster type based on team abbreviation.
|
|
|
|
CRITICAL: Handles edge cases like "BHMIL" (BHM + IL) vs "NYYMIL" (NYY + MIL).
|
|
Uses sname to disambiguate when abbreviation ends in "MIL".
|
|
|
|
Returns:
|
|
RosterType based on team abbreviation pattern
|
|
|
|
Examples:
|
|
- "NYY" (3 chars) → MAJOR_LEAGUE
|
|
- "NYYMIL" with sname "RailRiders" → MINOR_LEAGUE
|
|
- "BHMIL" with sname "Iron IL" → INJURED_LIST (BHM + IL, not BH + MIL)
|
|
- "NYYIL" → INJURED_LIST
|
|
"""
|
|
if len(self.abbrev) <= 3:
|
|
return RosterType.MAJOR_LEAGUE
|
|
|
|
abbrev_upper = self.abbrev.upper()
|
|
|
|
# Handle ambiguous "MIL" ending which could be:
|
|
# 1. [Team] + "MIL" (Minor League) - e.g., "NYYMIL"
|
|
# 2. [Team ending in M] + "IL" (Injured List) - e.g., "BHMIL" = "BHM" + "IL"
|
|
if abbrev_upper.endswith('MIL'):
|
|
# Use sname to disambiguate - IL teams have "IL" in their name
|
|
# But check for word boundaries to avoid matching "Island", "Illinois", etc.
|
|
if (self.sname.endswith('IL') or
|
|
self.sname.endswith(' IL') or
|
|
' IL ' in self.sname):
|
|
# This is actually a [Team]IL roster (e.g., "BHMIL" = "BHM" + "IL")
|
|
return RosterType.INJURED_LIST
|
|
# Otherwise it's a genuine Minor League team (e.g., "NYYMIL" = "NYY" + "MIL")
|
|
return RosterType.MINOR_LEAGUE
|
|
|
|
# Check for Injured List (2-char suffix "IL" that doesn't end in "MIL")
|
|
if abbrev_upper.endswith('IL'):
|
|
return RosterType.INJURED_LIST
|
|
|
|
# Default to Major League for any other pattern
|
|
return RosterType.MAJOR_LEAGUE
|
|
|
|
def _get_base_abbrev(self) -> str:
|
|
"""
|
|
Extract the base team abbreviation from potentially extended abbreviation.
|
|
|
|
Returns:
|
|
Base team abbreviation (typically 3 characters)
|
|
"""
|
|
abbrev_lower = self.abbrev.lower()
|
|
|
|
# If 3 chars or less, it's already the base team
|
|
if len(self.abbrev) <= 3:
|
|
return self.abbrev
|
|
|
|
# Handle teams ending in 'mil' - use sname to determine if IL or MiL
|
|
if abbrev_lower.endswith('mil'):
|
|
# If "IL" is in sname and abbrev ends in "IL" → It's [Team]IL
|
|
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
|
|
return self.abbrev[:-2] # Remove 'IL'
|
|
# Otherwise it's minor league → remove 'MIL'
|
|
return self.abbrev[:-3]
|
|
|
|
# Handle injured list: ends with 'il' but not 'mil'
|
|
if abbrev_lower.endswith('il'):
|
|
return self.abbrev[:-2] # Remove 'IL'
|
|
|
|
# Unknown pattern, return as-is
|
|
return self.abbrev
|
|
|
|
async def major_league_affiliate(self) -> 'Team':
|
|
"""
|
|
Get the major league team for this organization via API call.
|
|
|
|
Returns:
|
|
Team instance representing the major league affiliate
|
|
|
|
Raises:
|
|
APIException: If the affiliate team cannot be found
|
|
"""
|
|
from services.team_service import team_service
|
|
|
|
base_abbrev = self._get_base_abbrev()
|
|
if base_abbrev == self.abbrev:
|
|
return self # Already the major league team
|
|
|
|
team = await team_service.get_team_by_abbrev(base_abbrev, self.season)
|
|
if team is None:
|
|
raise ValueError(f"Major league affiliate not found for team {self.abbrev} (looking for {base_abbrev})")
|
|
return team
|
|
|
|
async def minor_league_affiliate(self) -> 'Team':
|
|
"""
|
|
Get the minor league team for this organization via API call.
|
|
|
|
Returns:
|
|
Team instance representing the minor league affiliate
|
|
|
|
Raises:
|
|
APIException: If the affiliate team cannot be found
|
|
"""
|
|
from services.team_service import team_service
|
|
|
|
base_abbrev = self._get_base_abbrev()
|
|
mil_abbrev = f"{base_abbrev}MIL"
|
|
|
|
if mil_abbrev == self.abbrev:
|
|
return self # Already the minor league team
|
|
|
|
team = await team_service.get_team_by_abbrev(mil_abbrev, self.season)
|
|
if team is None:
|
|
raise ValueError(f"Minor league affiliate not found for team {self.abbrev} (looking for {mil_abbrev})")
|
|
return team
|
|
|
|
async def injured_list_affiliate(self) -> 'Team':
|
|
"""
|
|
Get the injured list team for this organization via API call.
|
|
|
|
Returns:
|
|
Team instance representing the injured list affiliate
|
|
|
|
Raises:
|
|
APIException: If the affiliate team cannot be found
|
|
"""
|
|
from services.team_service import team_service
|
|
|
|
base_abbrev = self._get_base_abbrev()
|
|
il_abbrev = f"{base_abbrev}IL"
|
|
|
|
if il_abbrev == self.abbrev:
|
|
return self # Already the injured list team
|
|
|
|
team = await team_service.get_team_by_abbrev(il_abbrev, self.season)
|
|
if team is None:
|
|
raise ValueError(f"Injured list affiliate not found for team {self.abbrev} (looking for {il_abbrev})")
|
|
return team
|
|
|
|
def is_same_organization(self, other_team: 'Team') -> bool:
|
|
"""
|
|
Check if this team and another team are from the same organization.
|
|
|
|
Args:
|
|
other_team: Another team to compare
|
|
|
|
Returns:
|
|
True if both teams are from the same organization
|
|
"""
|
|
return self._get_base_abbrev() == other_team._get_base_abbrev()
|
|
|
|
def gm_names(self) -> str:
|
|
if any([self.manager1, self.manager2]):
|
|
names = ''
|
|
if self.manager1:
|
|
names += f'{self.manager1}'
|
|
if self.manager2:
|
|
names += f', {self.manager2}'
|
|
return names
|
|
if any([self.manager1_id, self.manager2_id]):
|
|
mgr_count = sum(1 for x in [self.manager1_id, self.manager2_id] if x is not None)
|
|
return f'{mgr_count} GM{"s" if mgr_count > 1 else ""}'
|
|
return 'Unknown'
|
|
|
|
def get_color_int(self, default_color: Optional[str] = None) -> int:
|
|
if self.color is not None:
|
|
return int(self.color, 16)
|
|
if default_color is not None:
|
|
return int(default_color, 16)
|
|
config = get_config()
|
|
return int(config.sba_color, 16)
|
|
|
|
def __str__(self):
|
|
return f"{self.abbrev} - {self.lname}" |