CLAUDE: Add draft period restriction to interactive draft commands

Restrict interactive draft commands to offseason only (week <= 0) while keeping read-only commands available year-round.

Changes:
- Add @requires_draft_period decorator to utils/decorators.py
- Apply decorator to /draft and all /draft-list-* commands
- Keep /draft-board, /draft-status, /draft-on-clock unrestricted
- Update commands/draft/CLAUDE.md with restriction documentation

Technical details:
- Decorator checks league_service.get_current_state().week <= 0
- Shows user-friendly error: "Draft commands are only available in the offseason"
- Follows existing @logged_command decorator pattern
- No Discord command cache delays - instant restriction

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-10-25 20:29:16 -05:00
parent d3824d7295
commit fb78b4b8c6
4 changed files with 121 additions and 2 deletions

View File

@ -4,11 +4,40 @@
This directory contains Discord slash commands for draft system operations. This directory contains Discord slash commands for draft system operations.
## 🚨 Important: Draft Period Restriction
**Interactive draft commands are restricted to the offseason (week ≤ 0).**
### Restricted Commands
The following commands can **only be used during the offseason** (when league week ≤ 0):
- `/draft` - Make draft picks
- `/draft-list` - View auto-draft queue
- `/draft-list-add` - Add player to queue
- `/draft-list-remove` - Remove player from queue
- `/draft-list-clear` - Clear entire queue
**Implementation:** The `@requires_draft_period` decorator automatically checks the current league week and returns an error message if the league is in-season.
### Unrestricted Commands
These commands remain **available year-round**:
- `/draft-board` - View draft picks by round
- `/draft-status` - View current draft state
- `/draft-on-clock` - View detailed on-the-clock information
- All `/draft-admin` commands (administrator only)
### User Experience
When a user tries to run a restricted command during the season, they see:
```
❌ Not Available
Draft commands are only available in the offseason.
```
## Files ## Files
### `picks.py` ### `picks.py`
- **Command**: `/draft` - **Command**: `/draft`
- **Description**: Make a draft pick with FA player autocomplete - **Description**: Make a draft pick with FA player autocomplete
- **Restriction**: Offseason only (week ≤ 0) via `@requires_draft_period` decorator
- **Parameters**: - **Parameters**:
- `player` (required): Player name to draft (autocomplete shows FA players with position and sWAR) - `player` (required): Player name to draft (autocomplete shows FA players with position and sWAR)
- **Service Dependencies**: - **Service Dependencies**:
@ -19,6 +48,7 @@ This directory contains Discord slash commands for draft system operations.
- `team_service.get_team_roster()` - `team_service.get_team_roster()`
- `player_service.get_players_by_name()` - `player_service.get_players_by_name()`
- `player_service.update_player_team()` - `player_service.update_player_team()`
- `league_service.get_current_state()` (for period check)
## Key Features ## Key Features

View File

@ -14,7 +14,7 @@ from services.draft_list_service import draft_list_service
from services.player_service import player_service from services.player_service import player_service
from services.team_service import team_service from services.team_service import team_service
from utils.logging import get_contextual_logger from utils.logging import get_contextual_logger
from utils.decorators import logged_command from utils.decorators import logged_command, requires_draft_period
from views.draft_views import create_draft_list_embed from views.draft_views import create_draft_list_embed
from views.embeds import EmbedTemplate from views.embeds import EmbedTemplate
@ -61,6 +61,7 @@ class DraftListCommands(commands.Cog):
name="draft-list", name="draft-list",
description="View your team's auto-draft queue" description="View your team's auto-draft queue"
) )
@requires_draft_period
@logged_command("/draft-list") @logged_command("/draft-list")
async def draft_list_view(self, interaction: discord.Interaction): async def draft_list_view(self, interaction: discord.Interaction):
"""Display team's draft list.""" """Display team's draft list."""
@ -101,6 +102,7 @@ class DraftListCommands(commands.Cog):
rank="Position in queue (optional, adds to end if not specified)" rank="Position in queue (optional, adds to end if not specified)"
) )
@discord.app_commands.autocomplete(player=fa_player_autocomplete) @discord.app_commands.autocomplete(player=fa_player_autocomplete)
@requires_draft_period
@logged_command("/draft-list-add") @logged_command("/draft-list-add")
async def draft_list_add( async def draft_list_add(
self, self,
@ -207,6 +209,7 @@ class DraftListCommands(commands.Cog):
player="Player name to remove" player="Player name to remove"
) )
@discord.app_commands.autocomplete(player=fa_player_autocomplete) @discord.app_commands.autocomplete(player=fa_player_autocomplete)
@requires_draft_period
@logged_command("/draft-list-remove") @logged_command("/draft-list-remove")
async def draft_list_remove( async def draft_list_remove(
self, self,
@ -268,6 +271,7 @@ class DraftListCommands(commands.Cog):
name="draft-list-clear", name="draft-list-clear",
description="Clear your entire auto-draft queue" description="Clear your entire auto-draft queue"
) )
@requires_draft_period
@logged_command("/draft-list-clear") @logged_command("/draft-list-clear")
async def draft_list_clear(self, interaction: discord.Interaction): async def draft_list_clear(self, interaction: discord.Interaction):
"""Clear entire draft list.""" """Clear entire draft list."""

View File

@ -16,7 +16,7 @@ from services.draft_pick_service import draft_pick_service
from services.player_service import player_service from services.player_service import player_service
from services.team_service import team_service from services.team_service import team_service
from utils.logging import get_contextual_logger from utils.logging import get_contextual_logger
from utils.decorators import logged_command from utils.decorators import logged_command, requires_draft_period
from utils.draft_helpers import validate_cap_space, format_pick_display from utils.draft_helpers import validate_cap_space, format_pick_display
from views.draft_views import ( from views.draft_views import (
create_player_draft_card, create_player_draft_card,
@ -77,6 +77,7 @@ class DraftPicksCog(commands.Cog):
player="Player name to draft (autocomplete shows available FA players)" player="Player name to draft (autocomplete shows available FA players)"
) )
@discord.app_commands.autocomplete(player=fa_player_autocomplete) @discord.app_commands.autocomplete(player=fa_player_autocomplete)
@requires_draft_period
@logged_command("/draft") @logged_command("/draft")
async def draft_pick( async def draft_pick(
self, self,

View File

@ -12,6 +12,7 @@ from typing import List, Optional, Callable, Any
from utils.logging import set_discord_context, get_contextual_logger from utils.logging import set_discord_context, get_contextual_logger
cache_logger = logging.getLogger(f'{__name__}.CacheDecorators') cache_logger = logging.getLogger(f'{__name__}.CacheDecorators')
period_check_logger = logging.getLogger(f'{__name__}.PeriodCheckDecorators')
def logged_command( def logged_command(
@ -95,6 +96,89 @@ def logged_command(
return decorator return decorator
def requires_draft_period(func):
"""
Decorator to restrict commands to draft period (week <= 0).
This decorator checks if the league is in the draft period (offseason)
before allowing the command to execute. If the league is in-season,
it returns an error message to the user.
Example:
@discord.app_commands.command(name="draft")
@requires_draft_period
@logged_command("/draft")
async def draft_pick(self, interaction, player: str):
# Command only runs during draft period (week <= 0)
pass
Side Effects:
- Checks league current state via league_service
- Returns error embed if check fails
- Logs restriction events
Requirements:
- Must be applied to async methods with (self, interaction, ...) signature
- Should be placed before @logged_command decorator
- league_service must be available via import
"""
@wraps(func)
async def wrapper(self, interaction, *args, **kwargs):
# Import here to avoid circular imports
from services.league_service import league_service
from views.embeds import EmbedTemplate
try:
# Check current league state
current = await league_service.get_current_state()
if not current:
period_check_logger.error("Could not retrieve league state for draft period check")
embed = EmbedTemplate.error(
"System Error",
"Could not verify draft period status. Please try again later."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Check if in draft period (week <= 0)
if current.week > 0:
period_check_logger.info(
f"Draft command blocked - current week: {current.week}",
extra={
"user_id": interaction.user.id,
"command": func.__name__,
"current_week": current.week
}
)
embed = EmbedTemplate.error(
"Not Available",
"Draft commands are only available in the offseason."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Week <= 0, allow command to proceed
period_check_logger.debug(
f"Draft period check passed - week {current.week}",
extra={"user_id": interaction.user.id, "command": func.__name__}
)
return await func(self, interaction, *args, **kwargs)
except Exception as e:
period_check_logger.error(
f"Error in draft period check: {e}",
exc_info=True,
extra={"user_id": interaction.user.id, "command": func.__name__}
)
# Re-raise to let error handling in logged_command handle it
raise
# Preserve signature for Discord.py command registration
wrapper.__signature__ = inspect.signature(func) # type: ignore
return wrapper
def cached_api_call(ttl: Optional[int] = None, cache_key_suffix: str = ""): def cached_api_call(ttl: Optional[int] = None, cache_key_suffix: str = ""):
""" """
Decorator to add Redis caching to service methods that return List[T]. Decorator to add Redis caching to service methods that return List[T].