CLAUDE: Reorganize data storage and enhance team/roster displays
Standardize data file locations to data/ directory and improve command organization with better UI for team rosters, pagination for team lists, and refactored chart commands into logical command groups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3c66ada99d
commit
7aa454f619
386
COMMAND_LIST.md
Normal file
386
COMMAND_LIST.md
Normal file
@ -0,0 +1,386 @@
|
||||
# Discord Bot v2.0 - Complete Command List
|
||||
|
||||
**Generated:** January 2025
|
||||
**Bot Version:** 2.0
|
||||
**Total Commands:** 55+ slash commands
|
||||
|
||||
---
|
||||
|
||||
## 📊 League Information Commands
|
||||
|
||||
### `/league`
|
||||
Display current league status and information
|
||||
|
||||
### `/standings`
|
||||
Display league standings
|
||||
|
||||
### `/playoff-picture`
|
||||
Display current playoff picture
|
||||
|
||||
### `/schedule`
|
||||
Display game schedule
|
||||
|
||||
### `/results`
|
||||
Display recent game results
|
||||
|
||||
---
|
||||
|
||||
## 👥 Player Commands
|
||||
|
||||
### `/player <name>`
|
||||
Display player information and statistics
|
||||
- **Parameters:** Player name (autocomplete enabled)
|
||||
|
||||
---
|
||||
|
||||
## 🏟️ Team Commands
|
||||
|
||||
### `/team <abbrev> [season]`
|
||||
Display team information
|
||||
- **Parameters:**
|
||||
- `abbrev`: Team abbreviation (e.g., NYY, BOS, LAD)
|
||||
- `season`: Season to show (optional, defaults to current)
|
||||
|
||||
### `/teams [season]`
|
||||
List all teams in a season
|
||||
- **Parameters:**
|
||||
- `season`: Season to list (optional, defaults to current)
|
||||
|
||||
### `/roster <abbrev> [roster_type]`
|
||||
Display team roster
|
||||
- **Parameters:**
|
||||
- `abbrev`: Team abbreviation
|
||||
- `roster_type`: Current or Next week (optional)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Transaction Commands
|
||||
|
||||
### `/mymoves`
|
||||
View your pending and scheduled transactions
|
||||
|
||||
### `/legal`
|
||||
Check roster legality for current and next week
|
||||
|
||||
### `/dropadd`
|
||||
Build a transaction for next week
|
||||
|
||||
### `/cleartransaction`
|
||||
Clear your current transaction builder
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Trade Commands
|
||||
|
||||
### `/trade initiate <other_team>`
|
||||
Start a new trade with another team
|
||||
- **Parameters:**
|
||||
- `other_team`: Team abbreviation (autocomplete enabled)
|
||||
- **Creates:** Dedicated trade discussion channel
|
||||
|
||||
### `/trade add-team <other_team>`
|
||||
Add another team to your current trade (for 3+ team trades)
|
||||
- **Parameters:**
|
||||
- `other_team`: Team abbreviation (autocomplete enabled)
|
||||
|
||||
### `/trade add-player <player_name> <destination_team>`
|
||||
Add a player to the trade
|
||||
- **Parameters:**
|
||||
- `player_name`: Player name (autocomplete enabled)
|
||||
- `destination_team`: Team abbreviation (autocomplete enabled)
|
||||
|
||||
### `/trade supplementary <player_name> <destination>`
|
||||
Add a supplementary move within your organization for roster legality
|
||||
- **Parameters:**
|
||||
- `player_name`: Player name (autocomplete enabled)
|
||||
- `destination`: Major League, Minor League, or Free Agency
|
||||
|
||||
### `/trade view`
|
||||
View your current trade
|
||||
|
||||
### `/trade clear`
|
||||
Clear your current trade and delete associated channel
|
||||
|
||||
---
|
||||
|
||||
## 🎲 Dice Rolling Commands
|
||||
|
||||
### `/roll <dice>`
|
||||
Roll polyhedral dice using XdY notation (e.g., 2d6, 1d20, 3d8)
|
||||
- **Parameters:**
|
||||
- `dice`: Dice notation (e.g., "2d6", "1d6;2d6;1d20")
|
||||
|
||||
### `/ab`
|
||||
Roll baseball at-bat dice (1d6;2d6;1d20)
|
||||
|
||||
### `/scout <card_type>`
|
||||
Roll weighted scouting dice (1d6;2d6;1d20) based on card type
|
||||
- **Parameters:**
|
||||
- `card_type`: Batter (1-3 first d6) or Pitcher (4-6 first d6)
|
||||
|
||||
### `/fielding <position>`
|
||||
Roll Super Advanced fielding dice for a defensive position
|
||||
- **Parameters:**
|
||||
- `position`: C, 1B, 2B, 3B, SS, LF, CF, RF
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Utility Commands
|
||||
|
||||
### `/weather [team_abbrev]`
|
||||
Roll ballpark weather for a team
|
||||
- **Parameters:**
|
||||
- `team_abbrev`: Team abbreviation (optional, auto-detects from channel/user)
|
||||
|
||||
### `/charts <chart_name>`
|
||||
Display a gameplay chart or infographic
|
||||
- **Parameters:**
|
||||
- `chart_name`: Name of chart (autocomplete enabled)
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ Voice Channel Commands
|
||||
|
||||
### `/voice-channel public`
|
||||
Create a public voice channel for gameplay
|
||||
- **Auto-cleanup:** Deletes after 15 minutes of being empty
|
||||
|
||||
### `/voice-channel private`
|
||||
Create a private team vs team voice channel
|
||||
- **Permissions:** Only team members can speak, others can listen
|
||||
- **Auto-detection:** Automatically finds your opponent from schedule
|
||||
- **Auto-cleanup:** Deletes after 15 minutes of being empty
|
||||
|
||||
---
|
||||
|
||||
## 📝 Custom Commands
|
||||
|
||||
### `/cc <name>`
|
||||
Execute a custom command
|
||||
- **Parameters:**
|
||||
- `name`: Name of custom command to execute
|
||||
|
||||
### `/cc-create`
|
||||
Create a new custom command
|
||||
- Opens interactive modal for input
|
||||
|
||||
### `/cc-edit <name>`
|
||||
Edit one of your custom commands
|
||||
- **Parameters:**
|
||||
- `name`: Name of command to edit
|
||||
|
||||
### `/cc-delete <name>`
|
||||
Delete one of your custom commands
|
||||
- **Parameters:**
|
||||
- `name`: Name of command to delete
|
||||
|
||||
### `/cc-mine`
|
||||
View and manage your custom commands
|
||||
|
||||
### `/cc-list`
|
||||
Browse all custom commands
|
||||
|
||||
### `/cc-search`
|
||||
Advanced search for custom commands
|
||||
|
||||
### `/cc-info <name>`
|
||||
Get detailed information about a custom command
|
||||
- **Parameters:**
|
||||
- `name`: Name of command
|
||||
|
||||
---
|
||||
|
||||
## 📚 Help System Commands
|
||||
|
||||
### `/help [topic]`
|
||||
View help topics or list all available help
|
||||
- **Parameters:**
|
||||
- `topic`: Specific help topic (optional, autocomplete enabled)
|
||||
|
||||
### `/help-create`
|
||||
Create a new help topic (admin/help editor only)
|
||||
- Opens interactive modal for input
|
||||
|
||||
### `/help-edit <topic>`
|
||||
Edit an existing help topic (admin/help editor only)
|
||||
- **Parameters:**
|
||||
- `topic`: Topic name to edit (autocomplete enabled)
|
||||
|
||||
### `/help-delete <topic>`
|
||||
Delete a help topic (admin/help editor only)
|
||||
- **Parameters:**
|
||||
- `topic`: Topic name to delete (autocomplete enabled)
|
||||
|
||||
### `/help-list [category] [show_deleted]`
|
||||
Browse all help topics
|
||||
- **Parameters:**
|
||||
- `category`: Filter by category (optional)
|
||||
- `show_deleted`: Include deleted topics (optional, admin only)
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Profile Management Commands
|
||||
|
||||
### `/set-image <image_type> <player_name> <image_url>`
|
||||
Update a player's fancy card or headshot image
|
||||
- **Parameters:**
|
||||
- `image_type`: Fancy Card or Headshot
|
||||
- `player_name`: Player name (autocomplete enabled)
|
||||
- `image_url`: URL to image
|
||||
- **Permissions:**
|
||||
- Regular users: Can update players in their organization (ML/MiL/IL)
|
||||
- Administrators: Can update any player
|
||||
- **Validation:** Checks URL accessibility and content-type
|
||||
|
||||
---
|
||||
|
||||
## 🎭 Meme Commands
|
||||
|
||||
### `/lastsoak`
|
||||
Get information about the last soak mention
|
||||
- Displays last player to say the forbidden word
|
||||
- Shows disappointment GIF
|
||||
- Tracks total mentions
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Admin Commands
|
||||
|
||||
### `/admin-status`
|
||||
Display bot status and system information
|
||||
|
||||
### `/admin-help`
|
||||
Display available admin commands and their usage
|
||||
|
||||
### `/admin-reload <cog>`
|
||||
Reload a specific bot cog
|
||||
- **Parameters:**
|
||||
- `cog`: Name of cog to reload
|
||||
|
||||
### `/admin-sync`
|
||||
Sync application commands with Discord
|
||||
|
||||
### `/admin-clear <amount>`
|
||||
Clear messages from the current channel
|
||||
- **Parameters:**
|
||||
- `amount`: Number of messages to clear
|
||||
|
||||
### `/admin-announce <message>`
|
||||
Send an announcement to the current channel
|
||||
- **Parameters:**
|
||||
- `message`: Announcement text
|
||||
|
||||
### `/admin-maintenance`
|
||||
Toggle maintenance mode for the bot
|
||||
|
||||
### `/admin-timeout <user> <duration> [reason]`
|
||||
Timeout a user for a specified duration
|
||||
- **Parameters:**
|
||||
- `user`: User to timeout
|
||||
- `duration`: Duration (e.g., "10m", "1h", "1d")
|
||||
- `reason`: Optional reason
|
||||
|
||||
### `/admin-untimeout <user>`
|
||||
Remove timeout from a user
|
||||
- **Parameters:**
|
||||
- `user`: User to remove timeout from
|
||||
|
||||
### `/admin-kick <user> [reason]`
|
||||
Kick a user from the server
|
||||
- **Parameters:**
|
||||
- `user`: User to kick
|
||||
- `reason`: Optional reason
|
||||
|
||||
### `/admin-ban <user> [reason]`
|
||||
Ban a user from the server
|
||||
- **Parameters:**
|
||||
- `user`: User to ban
|
||||
- `reason`: Optional reason
|
||||
|
||||
### `/admin-unban <user_id>`
|
||||
Unban a user from the server
|
||||
- **Parameters:**
|
||||
- `user_id`: Discord user ID to unban
|
||||
|
||||
### `/admin-userinfo <user>`
|
||||
Display detailed information about a user
|
||||
- **Parameters:**
|
||||
- `user`: User to get info about
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Admin - Chart Management Commands
|
||||
|
||||
### `/chart-add <key> <name> <category> <url> [description]`
|
||||
[Admin] Add a new chart to the library
|
||||
- **Parameters:**
|
||||
- `key`: Unique identifier (e.g., 'rest', 'sac-bunt')
|
||||
- `name`: Display name
|
||||
- `category`: gameplay, defense, reference, stats
|
||||
- `url`: Image URL
|
||||
- `description`: Optional description
|
||||
|
||||
### `/chart-remove <key>`
|
||||
[Admin] Remove a chart from the library
|
||||
- **Parameters:**
|
||||
- `key`: Chart key to remove
|
||||
|
||||
### `/chart-list [category]`
|
||||
[Admin] List all available charts
|
||||
- **Parameters:**
|
||||
- `category`: Filter by category (optional)
|
||||
|
||||
### `/chart-update <key> [name] [category] [url] [description]`
|
||||
[Admin] Update a chart's properties
|
||||
- **Parameters:**
|
||||
- `key`: Chart key to update
|
||||
- All other parameters are optional updates
|
||||
|
||||
---
|
||||
|
||||
## 📊 Command Statistics
|
||||
|
||||
- **Total Slash Commands:** 55+
|
||||
- **Command Groups:** 2 (`/voice-channel`, `/trade`)
|
||||
- **Admin Commands:** 16
|
||||
- **User Commands:** 39+
|
||||
- **Autocomplete Enabled:** 15+ commands
|
||||
- **Interactive Modals:** 4 commands (cc-create, cc-edit, help-create, help-edit)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### Autocomplete Support
|
||||
Commands with autocomplete for better UX:
|
||||
- Player names
|
||||
- Team abbreviations
|
||||
- Chart names
|
||||
- Help topics
|
||||
- Custom command names
|
||||
|
||||
### Interactive Features
|
||||
- Trade builder with real-time validation
|
||||
- Trade discussion channels
|
||||
- Custom command modals
|
||||
- Help topic modals
|
||||
- Confirmation dialogs
|
||||
|
||||
### Auto-cleanup Services
|
||||
- Voice channels (15 min empty threshold)
|
||||
- Trade discussion channels (on trade clear)
|
||||
|
||||
### Permission System
|
||||
- User-level permissions (own organization players)
|
||||
- Admin-level permissions (full access)
|
||||
- Role-based permissions (Help Editor role)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- All commands use modern Discord slash command syntax (`/command`)
|
||||
- Deprecated prefix commands (`!command`) show migration messages
|
||||
- Most commands use ephemeral responses for privacy
|
||||
- Comprehensive error handling and validation
|
||||
- Full logging with trace IDs for debugging
|
||||
@ -56,7 +56,7 @@ This document outlines the remaining functionality required before the Discord B
|
||||
- Admin commands for chart management (add, remove, list, update)
|
||||
- Category organization (gameplay, defense, reference, stats)
|
||||
- Proper embed formatting with descriptions
|
||||
- **Data Storage**: `storage/charts.json` with JSON persistence
|
||||
- **Data Storage**: `data/charts.json` with JSON persistence
|
||||
- **Completed**: January 2025
|
||||
|
||||
#### 4. Custom Help System **✅ COMPLETED**
|
||||
|
||||
@ -22,8 +22,8 @@ class LeagueInfoCommands(commands.Cog):
|
||||
self.logger = get_contextual_logger(f'{__name__}.LeagueInfoCommands')
|
||||
self.logger.info("LeagueInfoCommands cog initialized")
|
||||
|
||||
@discord.app_commands.command(name="league", description="Display current league status and information")
|
||||
@logged_command("/league")
|
||||
@discord.app_commands.command(name="league-metadata", description="Display current league metadata")
|
||||
@logged_command("/league-metadata")
|
||||
async def league_info(self, interaction: discord.Interaction):
|
||||
"""Display current league state and information."""
|
||||
await interaction.response.defer()
|
||||
@ -41,8 +41,8 @@ class LeagueInfoCommands(commands.Cog):
|
||||
|
||||
# Create league info embed
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="🏆 SBA League Status",
|
||||
description="Current league information and status"
|
||||
title="🏆 SBA League Metadata",
|
||||
description="Current league metadata"
|
||||
)
|
||||
|
||||
# Basic league info
|
||||
|
||||
@ -55,53 +55,53 @@ class ScheduleCommands(commands.Cog):
|
||||
# Show recent/upcoming games
|
||||
await self._show_current_schedule(interaction, search_season)
|
||||
|
||||
@discord.app_commands.command(
|
||||
name="results",
|
||||
description="Display recent game results"
|
||||
)
|
||||
@discord.app_commands.describe(
|
||||
season="Season to show results for (defaults to current season)",
|
||||
week="Specific week to show results for (optional)"
|
||||
)
|
||||
@logged_command("/results")
|
||||
async def results(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
season: Optional[int] = None,
|
||||
week: Optional[int] = None
|
||||
):
|
||||
"""Display recent game results."""
|
||||
await interaction.response.defer()
|
||||
# @discord.app_commands.command(
|
||||
# name="results",
|
||||
# description="Display recent game results"
|
||||
# )
|
||||
# @discord.app_commands.describe(
|
||||
# season="Season to show results for (defaults to current season)",
|
||||
# week="Specific week to show results for (optional)"
|
||||
# )
|
||||
# @logged_command("/results")
|
||||
# async def results(
|
||||
# self,
|
||||
# interaction: discord.Interaction,
|
||||
# season: Optional[int] = None,
|
||||
# week: Optional[int] = None
|
||||
# ):
|
||||
# """Display recent game results."""
|
||||
# await interaction.response.defer()
|
||||
|
||||
search_season = season or SBA_CURRENT_SEASON
|
||||
# search_season = season or SBA_CURRENT_SEASON
|
||||
|
||||
if week:
|
||||
# Show specific week results
|
||||
games = await schedule_service.get_week_schedule(search_season, week)
|
||||
completed_games = [game for game in games if game.is_completed]
|
||||
# if week:
|
||||
# # Show specific week results
|
||||
# games = await schedule_service.get_week_schedule(search_season, week)
|
||||
# completed_games = [game for game in games if game.is_completed]
|
||||
|
||||
if not completed_games:
|
||||
await interaction.followup.send(
|
||||
f"❌ No completed games found for season {search_season}, week {week}.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
# if not completed_games:
|
||||
# await interaction.followup.send(
|
||||
# f"❌ No completed games found for season {search_season}, week {week}.",
|
||||
# ephemeral=True
|
||||
# )
|
||||
# return
|
||||
|
||||
embed = await self._create_week_results_embed(completed_games, search_season, week)
|
||||
await interaction.followup.send(embed=embed)
|
||||
else:
|
||||
# Show recent results
|
||||
recent_games = await schedule_service.get_recent_games(search_season)
|
||||
# embed = await self._create_week_results_embed(completed_games, search_season, week)
|
||||
# await interaction.followup.send(embed=embed)
|
||||
# else:
|
||||
# # Show recent results
|
||||
# recent_games = await schedule_service.get_recent_games(search_season)
|
||||
|
||||
if not recent_games:
|
||||
await interaction.followup.send(
|
||||
f"❌ No recent games found for season {search_season}.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
# if not recent_games:
|
||||
# await interaction.followup.send(
|
||||
# f"❌ No recent games found for season {search_season}.",
|
||||
# ephemeral=True
|
||||
# )
|
||||
# return
|
||||
|
||||
embed = await self._create_recent_results_embed(recent_games, search_season)
|
||||
await interaction.followup.send(embed=embed)
|
||||
# embed = await self._create_recent_results_embed(recent_games, search_season)
|
||||
# await interaction.followup.send(embed=embed)
|
||||
|
||||
async def _show_week_schedule(self, interaction: discord.Interaction, season: int, week: int):
|
||||
"""Show schedule for a specific week."""
|
||||
|
||||
@ -172,23 +172,23 @@ class StandingsCommands(commands.Cog):
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Add additional stats for top teams
|
||||
if len(teams) >= 3:
|
||||
stats_lines = []
|
||||
for team in teams[:3]: # Top 3 teams
|
||||
stats_line = (
|
||||
f"**{team.team.abbrev}**: "
|
||||
f"Home {team.home_record} • "
|
||||
f"Last 8: {team.last8_record} • "
|
||||
f"Streak: {team.current_streak}"
|
||||
)
|
||||
stats_lines.append(stats_line)
|
||||
# # Add additional stats for top teams
|
||||
# if len(teams) >= 3:
|
||||
# stats_lines = []
|
||||
# for team in teams[:3]: # Top 3 teams
|
||||
# stats_line = (
|
||||
# f"**{team.team.abbrev}**: "
|
||||
# f"Home {team.home_record} • "
|
||||
# f"Last 8: {team.last8_record} • "
|
||||
# f"Streak: {team.current_streak}"
|
||||
# )
|
||||
# stats_lines.append(stats_line)
|
||||
|
||||
embed.add_field(
|
||||
name="Recent Form (Top 3)",
|
||||
value="\n".join(stats_lines),
|
||||
inline=False
|
||||
)
|
||||
# embed.add_field(
|
||||
# name="Recent Form (Top 3)",
|
||||
# value="\n".join(stats_lines),
|
||||
# inline=False
|
||||
# )
|
||||
|
||||
embed.set_footer(text=f"Season {season}")
|
||||
return embed
|
||||
|
||||
@ -22,7 +22,7 @@ class SoakTracker:
|
||||
- Time-based calculations for disappointment tiers
|
||||
"""
|
||||
|
||||
def __init__(self, data_file: str = "storage/soak_data.json"):
|
||||
def __init__(self, data_file: str = "data/soak_data.json"):
|
||||
"""
|
||||
Initialize the soak tracker.
|
||||
|
||||
|
||||
@ -8,12 +8,13 @@ import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from services import team_service, player_service
|
||||
from models.team import Team
|
||||
from models.team import RosterType, Team
|
||||
from constants import SBA_CURRENT_SEASON
|
||||
from utils.logging import get_contextual_logger
|
||||
from utils.decorators import logged_command
|
||||
from exceptions import BotException
|
||||
from views.embeds import EmbedTemplate, EmbedColors
|
||||
from views.base import PaginationView
|
||||
|
||||
|
||||
class TeamInfoCommands(commands.Cog):
|
||||
@ -78,36 +79,53 @@ class TeamInfoCommands(commands.Cog):
|
||||
await interaction.followup.send(embed=embed)
|
||||
return
|
||||
|
||||
# Sort teams by abbreviation
|
||||
teams.sort(key=lambda t: t.abbrev)
|
||||
# Filter to major league teams only and sort by abbreviation
|
||||
ml_teams = [team for team in teams if team.roster_type() == RosterType.MAJOR_LEAGUE]
|
||||
ml_teams.sort(key=lambda t: t.abbrev)
|
||||
|
||||
if not ml_teams:
|
||||
embed = EmbedTemplate.error(
|
||||
title="No Major League Teams Found",
|
||||
description=f"No major league teams found for season {season}"
|
||||
)
|
||||
await interaction.followup.send(embed=embed)
|
||||
return
|
||||
|
||||
# Create paginated embeds (12 teams per page to stay under character limit)
|
||||
teams_per_page = 12
|
||||
pages: list[discord.Embed] = []
|
||||
|
||||
for i in range(0, len(ml_teams), teams_per_page):
|
||||
page_teams = ml_teams[i:i + teams_per_page]
|
||||
|
||||
# Create embed with team list
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"SBA Teams - Season {season}",
|
||||
color=EmbedColors.PRIMARY
|
||||
)
|
||||
|
||||
# Group teams by division if available
|
||||
if any(team.division_id for team in teams):
|
||||
divisions = {}
|
||||
for team in teams:
|
||||
div_id = team.division_id or 0
|
||||
if div_id not in divisions:
|
||||
divisions[div_id] = []
|
||||
divisions[div_id].append(team)
|
||||
for team in page_teams:
|
||||
embed.add_field(
|
||||
name=f'{team}',
|
||||
value=self._team_detail_description(team),
|
||||
inline=False
|
||||
)
|
||||
|
||||
for div_id, div_teams in sorted(divisions.items()):
|
||||
div_name = f"Division {div_id}" if div_id > 0 else "Unassigned"
|
||||
team_list = "\n".join([f"**{team.abbrev}** - {team.lname}" for team in div_teams])
|
||||
embed.add_field(name=div_name, value=team_list, inline=True)
|
||||
embed.set_footer(text=f"Total: {len(ml_teams)} teams")
|
||||
pages.append(embed)
|
||||
|
||||
# Use pagination if multiple pages, otherwise send single embed
|
||||
if len(pages) > 1:
|
||||
pagination = PaginationView(
|
||||
pages=pages,
|
||||
user_id=interaction.user.id,
|
||||
show_page_numbers=True
|
||||
)
|
||||
await interaction.followup.send(embed=pagination.get_current_embed(), view=pagination)
|
||||
else:
|
||||
# Simple list if no divisions
|
||||
team_list = "\n".join([f"**{team.abbrev}** - {team.lname}" for team in teams])
|
||||
embed.add_field(name="Teams", value=team_list, inline=False)
|
||||
await interaction.followup.send(embed=pages[0])
|
||||
|
||||
embed.set_footer(text=f"Total: {len(teams)} teams")
|
||||
|
||||
await interaction.followup.send(embed=embed)
|
||||
def _team_detail_description(self, team: Team) -> str:
|
||||
return f'GM: {team.gm_names()}\nID: {team.id}'
|
||||
|
||||
async def _create_team_embed(self, team: Team, standings_data: Optional[dict] = None) -> discord.Embed:
|
||||
"""Create a rich embed for team information."""
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Optional, Dict, Any, List
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from models.player import Player
|
||||
from services import team_service, player_service
|
||||
from models.team import Team
|
||||
from constants import SBA_CURRENT_SEASON
|
||||
@ -26,7 +27,7 @@ class TeamRosterCommands(commands.Cog):
|
||||
|
||||
@discord.app_commands.command(name="roster", description="Display team roster")
|
||||
@discord.app_commands.describe(
|
||||
abbrev="Team abbreviation (e.g., NYY, BOS, LAD)",
|
||||
abbrev="Team abbreviation (e.g., BSG, DEN, WV, etc.)",
|
||||
roster_type="Roster week: current or next (defaults to current)"
|
||||
)
|
||||
@discord.app_commands.choices(roster_type=[
|
||||
@ -77,64 +78,51 @@ class TeamRosterCommands(commands.Cog):
|
||||
|
||||
# Main roster embed
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"{team.abbrev} - {roster_type.title()} Roster",
|
||||
description=f"{team.lname} roster breakdown",
|
||||
title=f"{team.abbrev} - {roster_type.title()} Week",
|
||||
description=f"{team.lname} Roster Breakdown",
|
||||
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY
|
||||
)
|
||||
|
||||
# Position counts for active roster
|
||||
if 'active' in roster_data:
|
||||
active_roster = roster_data['active']
|
||||
for key in ['active', 'longil', 'shortil']:
|
||||
if key in roster_data:
|
||||
this_roster = roster_data[key]
|
||||
|
||||
# Batting positions
|
||||
batting_positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
|
||||
batting_counts = []
|
||||
for pos in batting_positions:
|
||||
count = active_roster.get(pos, 0)
|
||||
batting_counts.append(f"**{pos}:** {count}")
|
||||
players = this_roster.get('players')
|
||||
if len(players) > 0:
|
||||
this_team = players[0].get("team", {"id": "Unknown", "sname": "Unknown"})
|
||||
|
||||
# Pitching positions
|
||||
pitching_positions = ['SP', 'RP', 'CP']
|
||||
pitching_counts = []
|
||||
for pos in pitching_positions:
|
||||
count = active_roster.get(pos, 0)
|
||||
pitching_counts.append(f"**{pos}:** {count}")
|
||||
|
||||
# Add position count fields
|
||||
embed.add_field(
|
||||
name="Batting Positions",
|
||||
value="\n".join(batting_counts),
|
||||
name='Team (ID)',
|
||||
value=f'{this_team.get("sname")} ({this_team.get("id")})',
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Pitching Positions",
|
||||
value="\n".join(pitching_counts),
|
||||
inline=True
|
||||
name='Player Count',
|
||||
value=f'{len(players)} Players'
|
||||
)
|
||||
|
||||
# Total WAR
|
||||
total_war = active_roster.get('WARa', 0)
|
||||
total_war = this_roster.get('WARa', 0)
|
||||
embed.add_field(
|
||||
name="Total sWAR",
|
||||
value=f"{total_war:.1f}" if isinstance(total_war, (int, float)) else str(total_war),
|
||||
value=f"{total_war:.2f}" if isinstance(total_war, (int, float)) else str(total_war),
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Add injury list summaries
|
||||
if 'shortil' in roster_data and roster_data['shortil']:
|
||||
short_il_count = len(roster_data['shortil'].get('players', []))
|
||||
embed.add_field(name="Minor League", value=f"{short_il_count} players", inline=True)
|
||||
|
||||
if 'longil' in roster_data and roster_data['longil']:
|
||||
long_il_count = len(roster_data['longil'].get('players', []))
|
||||
embed.add_field(name="Injured List", value=f"{long_il_count} players", inline=True)
|
||||
embed.add_field(
|
||||
name='Position Counts',
|
||||
value=self._position_code_block(this_roster),
|
||||
inline=False
|
||||
)
|
||||
|
||||
embeds.append(embed)
|
||||
|
||||
# Create detailed player list embeds if there are players
|
||||
for roster_name, roster_info in roster_data.items():
|
||||
if roster_name in ['active', 'shortil', 'longil'] and 'players' in roster_info:
|
||||
players = roster_info['players']
|
||||
if roster_name in ['active', 'longil', 'shortil'] and 'players' in roster_info:
|
||||
players = sorted(roster_info['players'], key=lambda player: player.get('wara', 0), reverse=True)
|
||||
if players:
|
||||
player_embed = self._create_player_list_embed(
|
||||
team, roster_name, players
|
||||
@ -143,13 +131,20 @@ class TeamRosterCommands(commands.Cog):
|
||||
|
||||
return embeds
|
||||
|
||||
def _position_code_block(self, roster_data: dict) -> str:
|
||||
return f'```\n C 1B 2B 3B SS\n' \
|
||||
f' {roster_data.get("C", 0)} {roster_data.get("1B", 0)} {roster_data.get("2B", 0)} ' \
|
||||
f'{roster_data.get("3B", 0)} {roster_data.get("SS", 0)}\n\nLF CF RF SP RP\n' \
|
||||
f' {roster_data.get("LF", 0)} {roster_data.get("CF", 0)} {roster_data.get("RF", 0)} ' \
|
||||
f'{roster_data.get("SP", 0)} {roster_data.get("RP", 0)}\n```'
|
||||
|
||||
def _create_player_list_embed(self, team: Team, roster_name: str,
|
||||
players: List[Dict[str, Any]]) -> discord.Embed:
|
||||
"""Create an embed with detailed player list."""
|
||||
roster_titles = {
|
||||
'active': 'Active Roster',
|
||||
'shortil': 'Minor League',
|
||||
'longil': 'Injured List'
|
||||
'longil': 'Minor League',
|
||||
'shortil': 'Injured List'
|
||||
}
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
@ -162,33 +157,28 @@ class TeamRosterCommands(commands.Cog):
|
||||
pitchers = []
|
||||
|
||||
for player in players:
|
||||
name = player.get('name', 'Unknown')
|
||||
positions = player.get('positions', [])
|
||||
war = player.get('WARa', 0)
|
||||
try:
|
||||
this_player = Player.from_api_data(player)
|
||||
player_line = f"{this_player} - sWAR: {this_player.wara}"
|
||||
|
||||
# Format WAR display
|
||||
war_str = f"{war:.1f}" if isinstance(war, (int, float)) else str(war)
|
||||
|
||||
# Determine if pitcher or batter
|
||||
is_pitcher = any(pos in ['SP', 'RP', 'CP'] for pos in positions)
|
||||
|
||||
player_line = f"**{name}** ({'/'.join(positions)}) - WAR: {war_str}"
|
||||
|
||||
if is_pitcher:
|
||||
if this_player.is_pitcher:
|
||||
pitchers.append(player_line)
|
||||
else:
|
||||
batters.append(player_line)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to create player from data: {e}", player_id=player.get('id'))
|
||||
|
||||
# Add player lists to embed
|
||||
if batters:
|
||||
# Split long lists into multiple fields if needed
|
||||
batter_chunks = self._chunk_list(batters, 10)
|
||||
batter_chunks = self._chunk_list(batters, 16)
|
||||
for i, chunk in enumerate(batter_chunks):
|
||||
field_name = "Batters" if i == 0 else f"Batters (cont.)"
|
||||
embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
|
||||
embed.add_field(name=field_name, value="\n".join(chunk), inline=True)
|
||||
embed.add_field(name='', value='', inline=False)
|
||||
|
||||
if pitchers:
|
||||
pitcher_chunks = self._chunk_list(pitchers, 10)
|
||||
pitcher_chunks = self._chunk_list(pitchers, 16)
|
||||
for i, chunk in enumerate(pitcher_chunks):
|
||||
field_name = "Pitchers" if i == 0 else f"Pitchers (cont.)"
|
||||
embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
|
||||
|
||||
@ -27,7 +27,7 @@ class TradeChannelTracker:
|
||||
- Automatic stale entry removal
|
||||
"""
|
||||
|
||||
def __init__(self, data_file: str = "storage/trade_channels.json"):
|
||||
def __init__(self, data_file: str = "data/trade_channels.json"):
|
||||
"""
|
||||
Initialize the trade channel tracker.
|
||||
|
||||
|
||||
@ -163,7 +163,7 @@ Administrators can manage the chart library using these commands:
|
||||
- **Files**:
|
||||
- `commands/utilities/charts.py` - Command handlers
|
||||
- `services/chart_service.py` - Chart management service
|
||||
- `storage/charts.json` - Chart definitions storage
|
||||
- `data/charts.json` - Chart definitions storage
|
||||
- **Service**: `ChartService` - Manages chart loading, saving, and retrieval
|
||||
- **Categories**: gameplay, defense, reference, stats
|
||||
- **Logging**: Uses `@logged_command` decorator for automatic logging
|
||||
@ -194,7 +194,7 @@ Administrators can manage the chart library using these commands:
|
||||
→ Shows all gameplay charts
|
||||
```
|
||||
|
||||
**Data Structure** (`storage/charts.json`):
|
||||
**Data Structure** (`data/charts.json`):
|
||||
```json
|
||||
{
|
||||
"charts": {
|
||||
|
||||
@ -7,9 +7,9 @@ import logging
|
||||
from discord.ext import commands
|
||||
|
||||
from .weather import WeatherCommands
|
||||
from .charts import ChartCommands, ChartAdminCommands
|
||||
from .charts import ChartCommands, ChartManageGroup, ChartCategoryGroup
|
||||
|
||||
__all__ = ['WeatherCommands', 'ChartCommands', 'ChartAdminCommands', 'setup_utilities']
|
||||
__all__ = ['WeatherCommands', 'ChartCommands', 'ChartManageGroup', 'ChartCategoryGroup', 'setup_utilities']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -28,10 +28,10 @@ async def setup_utilities(bot: commands.Bot) -> tuple[int, int, list[str]]:
|
||||
failed = 0
|
||||
failed_modules = []
|
||||
|
||||
# Cogs that need bot instance
|
||||
cog_classes = [
|
||||
WeatherCommands,
|
||||
ChartCommands,
|
||||
ChartAdminCommands,
|
||||
]
|
||||
|
||||
for cog_class in cog_classes:
|
||||
@ -44,4 +44,20 @@ async def setup_utilities(bot: commands.Bot) -> tuple[int, int, list[str]]:
|
||||
failed += 1
|
||||
failed_modules.append(cog_class.__name__)
|
||||
|
||||
# Command groups (added directly to command tree)
|
||||
command_groups = [
|
||||
ChartManageGroup,
|
||||
ChartCategoryGroup,
|
||||
]
|
||||
|
||||
for group_class in command_groups:
|
||||
try:
|
||||
bot.tree.add_command(group_class())
|
||||
logger.info(f"Loaded command group: {group_class.__name__}")
|
||||
successful += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load command group {group_class.__name__}: {e}", exc_info=True)
|
||||
failed += 1
|
||||
failed_modules.append(group_class.__name__)
|
||||
|
||||
return successful, failed, failed_modules
|
||||
|
||||
@ -14,23 +14,18 @@ from utils.logging import get_contextual_logger, set_discord_context
|
||||
from services.chart_service import get_chart_service, Chart
|
||||
from views.embeds import EmbedTemplate, EmbedColors
|
||||
from exceptions import BotException
|
||||
from constants import HELP_EDITOR_ROLE_NAME
|
||||
|
||||
|
||||
class ChartCommands(commands.Cog):
|
||||
"""Chart display command handlers."""
|
||||
# Standalone autocomplete functions
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.ChartCommands')
|
||||
self.chart_service = get_chart_service()
|
||||
|
||||
async def chart_autocomplete(
|
||||
self,
|
||||
async def chart_autocomplete(
|
||||
interaction: discord.Interaction,
|
||||
current: str
|
||||
) -> List[app_commands.Choice[str]]:
|
||||
) -> List[app_commands.Choice[str]]:
|
||||
"""Autocomplete for chart names."""
|
||||
chart_keys = self.chart_service.get_chart_keys()
|
||||
chart_service = get_chart_service()
|
||||
chart_keys = chart_service.get_chart_keys()
|
||||
|
||||
# Filter based on current input
|
||||
filtered = [
|
||||
@ -41,7 +36,7 @@ class ChartCommands(commands.Cog):
|
||||
# Get chart objects for display names
|
||||
choices = []
|
||||
for key in filtered:
|
||||
chart = self.chart_service.get_chart(key)
|
||||
chart = chart_service.get_chart(key)
|
||||
if chart:
|
||||
choices.append(
|
||||
app_commands.Choice(
|
||||
@ -52,6 +47,53 @@ class ChartCommands(commands.Cog):
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
async def category_autocomplete(
|
||||
interaction: discord.Interaction,
|
||||
current: str
|
||||
) -> List[app_commands.Choice[str]]:
|
||||
"""Autocomplete for category keys."""
|
||||
chart_service = get_chart_service()
|
||||
categories = chart_service.get_categories()
|
||||
|
||||
# Filter based on current input
|
||||
filtered = [
|
||||
key for key in categories.keys()
|
||||
if current.lower() in key.lower()
|
||||
][:25] # Discord limit
|
||||
|
||||
return [
|
||||
app_commands.Choice(
|
||||
name=f"{categories[key]} ({key})",
|
||||
value=key
|
||||
)
|
||||
for key in filtered
|
||||
]
|
||||
|
||||
|
||||
# Helper function for permission checking
|
||||
def has_manage_permission(interaction: discord.Interaction) -> bool:
|
||||
"""Check if user has permission to manage charts/categories."""
|
||||
# Check if user is admin
|
||||
if interaction.user.guild_permissions.administrator:
|
||||
return True
|
||||
|
||||
# Check if user has the Help Editor role
|
||||
help_editor_role = discord.utils.get(interaction.guild.roles, name=HELP_EDITOR_ROLE_NAME)
|
||||
if help_editor_role and help_editor_role in interaction.user.roles:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ChartCommands(commands.Cog):
|
||||
"""Chart display command handlers."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.ChartCommands')
|
||||
self.chart_service = get_chart_service()
|
||||
|
||||
@app_commands.command(
|
||||
name="charts",
|
||||
description="Display a gameplay chart or infographic"
|
||||
@ -112,115 +154,11 @@ class ChartCommands(commands.Cog):
|
||||
followup_embed.set_image(url=url)
|
||||
await interaction.followup.send(embed=followup_embed)
|
||||
|
||||
|
||||
class ChartAdminCommands(commands.Cog):
|
||||
"""Chart management command handlers for administrators."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.ChartAdminCommands')
|
||||
self.chart_service = get_chart_service()
|
||||
|
||||
@app_commands.command(
|
||||
name="chart-add",
|
||||
description="[Admin] Add a new chart to the library"
|
||||
)
|
||||
@app_commands.describe(
|
||||
key="Unique identifier for the chart (e.g., 'rest', 'sac-bunt')",
|
||||
name="Display name for the chart",
|
||||
category="Category (gameplay, defense, reference, stats)",
|
||||
url="Image URL for the chart",
|
||||
description="Optional description of the chart"
|
||||
)
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
@logged_command("/chart-add")
|
||||
async def chart_add(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str,
|
||||
name: str,
|
||||
category: str,
|
||||
url: str,
|
||||
description: Optional[str] = None
|
||||
):
|
||||
"""Add a new chart to the library."""
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-add",
|
||||
chart_key=key,
|
||||
chart_name=name
|
||||
)
|
||||
|
||||
# Validate category
|
||||
valid_categories = list(self.chart_service.get_categories().keys())
|
||||
if category not in valid_categories:
|
||||
raise BotException(
|
||||
f"Invalid category. Must be one of: {', '.join(valid_categories)}"
|
||||
)
|
||||
|
||||
# Add chart (service will handle duplicate key check)
|
||||
self.chart_service.add_chart(
|
||||
key=key,
|
||||
name=name,
|
||||
category=category,
|
||||
urls=[url],
|
||||
description=description or ""
|
||||
)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Chart Added",
|
||||
description=f"Successfully added chart '{name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Category", value=category, inline=True)
|
||||
embed.set_image(url=url)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="chart-remove",
|
||||
description="[Admin] Remove a chart from the library"
|
||||
)
|
||||
@app_commands.describe(key="Chart key to remove")
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
@logged_command("/chart-remove")
|
||||
async def chart_remove(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str
|
||||
):
|
||||
"""Remove a chart from the library."""
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-remove",
|
||||
chart_key=key
|
||||
)
|
||||
|
||||
# Get chart before removing (for confirmation message)
|
||||
chart = self.chart_service.get_chart(key)
|
||||
if chart is None:
|
||||
raise BotException(f"Chart '{key}' not found")
|
||||
|
||||
# Remove chart
|
||||
self.chart_service.remove_chart(key)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Chart Removed",
|
||||
description=f"Successfully removed chart '{chart.name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Category", value=chart.category, inline=True)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="chart-list",
|
||||
description="[Admin] List all available charts"
|
||||
description="List all available charts"
|
||||
)
|
||||
@app_commands.describe(category="Filter by category (optional)")
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
@logged_command("/chart-list")
|
||||
async def chart_list(
|
||||
self,
|
||||
@ -276,11 +214,135 @@ class ChartAdminCommands(commands.Cog):
|
||||
inline=False
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
|
||||
class ChartManageGroup(app_commands.Group):
|
||||
"""Chart management commands for administrators and help editors."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="chart-manage",
|
||||
description="Manage charts (admin/help editor only)"
|
||||
)
|
||||
self.logger = get_contextual_logger(f'{__name__}.ChartManageGroup')
|
||||
self.chart_service = get_chart_service()
|
||||
|
||||
@app_commands.command(
|
||||
name="add",
|
||||
description="Add a new chart to the library"
|
||||
)
|
||||
@app_commands.describe(
|
||||
key="Unique identifier for the chart (e.g., 'rest', 'sac-bunt')",
|
||||
name="Display name for the chart",
|
||||
category="Category key (use autocomplete)",
|
||||
url="Image URL for the chart",
|
||||
description="Optional description of the chart"
|
||||
)
|
||||
@app_commands.autocomplete(category=category_autocomplete)
|
||||
@logged_command("/chart-manage add")
|
||||
async def add(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str,
|
||||
name: str,
|
||||
category: str,
|
||||
url: str,
|
||||
description: Optional[str] = None
|
||||
):
|
||||
"""Add a new chart to the library."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage charts."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-manage add",
|
||||
chart_key=key,
|
||||
chart_name=name
|
||||
)
|
||||
|
||||
# Validate category
|
||||
valid_categories = list(self.chart_service.get_categories().keys())
|
||||
if category not in valid_categories:
|
||||
raise BotException(
|
||||
f"Invalid category. Must be one of: {', '.join(valid_categories)}"
|
||||
)
|
||||
|
||||
# Add chart (service will handle duplicate key check)
|
||||
self.chart_service.add_chart(
|
||||
key=key,
|
||||
name=name,
|
||||
category=category,
|
||||
urls=[url],
|
||||
description=description or ""
|
||||
)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Chart Added",
|
||||
description=f"Successfully added chart '{name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Category", value=category, inline=True)
|
||||
embed.set_image(url=url)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="chart-update",
|
||||
description="[Admin] Update a chart's properties"
|
||||
name="remove",
|
||||
description="Remove a chart from the library"
|
||||
)
|
||||
@app_commands.describe(key="Chart key to remove")
|
||||
@app_commands.autocomplete(key=chart_autocomplete)
|
||||
@logged_command("/chart-manage remove")
|
||||
async def remove(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str
|
||||
):
|
||||
"""Remove a chart from the library."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage charts."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-manage remove",
|
||||
chart_key=key
|
||||
)
|
||||
|
||||
# Get chart before removing (for confirmation message)
|
||||
chart = self.chart_service.get_chart(key)
|
||||
if chart is None:
|
||||
raise BotException(f"Chart '{key}' not found")
|
||||
|
||||
# Remove chart
|
||||
self.chart_service.remove_chart(key)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Chart Removed",
|
||||
description=f"Successfully removed chart '{chart.name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Category", value=chart.category, inline=True)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="update",
|
||||
description="Update a chart's properties"
|
||||
)
|
||||
@app_commands.describe(
|
||||
key="Chart key to update",
|
||||
@ -289,9 +351,12 @@ class ChartAdminCommands(commands.Cog):
|
||||
url="New image URL (optional)",
|
||||
description="New description (optional)"
|
||||
)
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
@logged_command("/chart-update")
|
||||
async def chart_update(
|
||||
@app_commands.autocomplete(
|
||||
key=chart_autocomplete,
|
||||
category=category_autocomplete
|
||||
)
|
||||
@logged_command("/chart-manage update")
|
||||
async def update(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str,
|
||||
@ -301,9 +366,18 @@ class ChartAdminCommands(commands.Cog):
|
||||
description: Optional[str] = None
|
||||
):
|
||||
"""Update a chart's properties."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage charts."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-update",
|
||||
command="/chart-manage update",
|
||||
chart_key=key
|
||||
)
|
||||
|
||||
@ -347,7 +421,219 @@ class ChartAdminCommands(commands.Cog):
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
class ChartCategoryGroup(app_commands.Group):
|
||||
"""Chart category management commands for administrators and help editors."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="chart-categories",
|
||||
description="Manage chart categories (admin/help editor only)"
|
||||
)
|
||||
self.logger = get_contextual_logger(f'{__name__}.ChartCategoryGroup')
|
||||
self.chart_service = get_chart_service()
|
||||
|
||||
@app_commands.command(
|
||||
name="list",
|
||||
description="List all chart categories"
|
||||
)
|
||||
@logged_command("/chart-categories list")
|
||||
async def list_categories(
|
||||
self,
|
||||
interaction: discord.Interaction
|
||||
):
|
||||
"""List all chart categories."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage categories."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-categories list"
|
||||
)
|
||||
|
||||
categories = self.chart_service.get_categories()
|
||||
|
||||
if not categories:
|
||||
embed = EmbedTemplate.info(
|
||||
title="📊 Chart Categories",
|
||||
description="No categories defined. Use `/chart-categories add` to create one."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
# Create embed
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="📊 Chart Categories",
|
||||
description=f"Total: {len(categories)} category(ies)",
|
||||
color=EmbedColors.PRIMARY
|
||||
)
|
||||
|
||||
# List all categories
|
||||
category_list = "\n".join([
|
||||
f"• `{key}` - {display_name}"
|
||||
for key, display_name in sorted(categories.items())
|
||||
])
|
||||
|
||||
embed.add_field(
|
||||
name="Categories",
|
||||
value=category_list,
|
||||
inline=False
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="add",
|
||||
description="Add a new chart category"
|
||||
)
|
||||
@app_commands.describe(
|
||||
key="Category key (e.g., 'gameplay', 'stats')",
|
||||
display_name="Display name (e.g., 'Gameplay Charts', 'Statistics')"
|
||||
)
|
||||
@logged_command("/chart-categories add")
|
||||
async def add_category(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str,
|
||||
display_name: str
|
||||
):
|
||||
"""Add a new chart category."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage categories."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-categories add",
|
||||
category_key=key
|
||||
)
|
||||
|
||||
# Add category (service will handle duplicate check)
|
||||
self.chart_service.add_category(key=key, display_name=display_name)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Category Added",
|
||||
description=f"Successfully added category '{display_name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Display Name", value=display_name, inline=True)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="remove",
|
||||
description="Remove a chart category"
|
||||
)
|
||||
@app_commands.describe(key="Category key to remove")
|
||||
@app_commands.autocomplete(key=category_autocomplete)
|
||||
@logged_command("/chart-categories remove")
|
||||
async def remove_category(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str
|
||||
):
|
||||
"""Remove a chart category."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage categories."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-categories remove",
|
||||
category_key=key
|
||||
)
|
||||
|
||||
# Get category before removing (for confirmation message)
|
||||
categories = self.chart_service.get_categories()
|
||||
if key not in categories:
|
||||
raise BotException(f"Category '{key}' not found")
|
||||
|
||||
category_display = categories[key]
|
||||
|
||||
# Remove category (service will validate no charts use it)
|
||||
self.chart_service.remove_category(key)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Category Removed",
|
||||
description=f"Successfully removed category '{category_display}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@app_commands.command(
|
||||
name="rename",
|
||||
description="Rename a chart category"
|
||||
)
|
||||
@app_commands.describe(
|
||||
key="Category key to rename",
|
||||
new_display_name="New display name"
|
||||
)
|
||||
@app_commands.autocomplete(key=category_autocomplete)
|
||||
@logged_command("/chart-categories rename")
|
||||
async def rename_category(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
key: str,
|
||||
new_display_name: str
|
||||
):
|
||||
"""Rename a chart category."""
|
||||
# Check permissions
|
||||
if not has_manage_permission(interaction):
|
||||
embed = EmbedTemplate.error(
|
||||
title="Permission Denied",
|
||||
description=f"Only administrators and users with the **{HELP_EDITOR_ROLE_NAME}** role can manage categories."
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
set_discord_context(
|
||||
interaction=interaction,
|
||||
command="/chart-categories rename",
|
||||
category_key=key
|
||||
)
|
||||
|
||||
# Get old name for confirmation
|
||||
categories = self.chart_service.get_categories()
|
||||
if key not in categories:
|
||||
raise BotException(f"Category '{key}' not found")
|
||||
|
||||
old_display_name = categories[key]
|
||||
|
||||
# Update category
|
||||
self.chart_service.update_category(key=key, display_name=new_display_name)
|
||||
|
||||
# Success response
|
||||
embed = EmbedTemplate.success(
|
||||
title="Category Renamed",
|
||||
description=f"Successfully renamed category from '{old_display_name}' to '{new_display_name}'"
|
||||
)
|
||||
embed.add_field(name="Key", value=key, inline=True)
|
||||
embed.add_field(name="Old Name", value=old_display_name, inline=True)
|
||||
embed.add_field(name="New Name", value=new_display_name, inline=True)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
"""Setup function for chart commands."""
|
||||
await bot.add_cog(ChartCommands(bot))
|
||||
await bot.add_cog(ChartAdminCommands(bot))
|
||||
bot.tree.add_command(ChartManageGroup())
|
||||
bot.tree.add_command(ChartCategoryGroup())
|
||||
|
||||
@ -146,7 +146,7 @@ if hasattr(self.bot, 'voice_cleanup_service'):
|
||||
### Cleanup Service Settings
|
||||
- **`cleanup_interval`**: How often to check channels (default: 60 seconds)
|
||||
- **`empty_threshold`**: Minutes empty before deletion (default: 5 minutes)
|
||||
- **`data_file`**: JSON persistence file path (default: "storage/voice_channels.json")
|
||||
- **`data_file`**: JSON persistence file path (default: "data/voice_channels.json")
|
||||
|
||||
### Channel Categories
|
||||
- Channels are created in the "Voice Channels" category if it exists
|
||||
|
||||
@ -25,7 +25,7 @@ class VoiceChannelCleanupService:
|
||||
- Stale entry removal and recovery
|
||||
"""
|
||||
|
||||
def __init__(self, data_file: str = "storage/voice_channels.json"):
|
||||
def __init__(self, data_file: str = "data/voice_channels.json"):
|
||||
"""
|
||||
Initialize the cleanup service.
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ class VoiceChannelTracker:
|
||||
- Automatic stale entry removal
|
||||
"""
|
||||
|
||||
def __init__(self, data_file: str = "storage/voice_channels.json"):
|
||||
def __init__(self, data_file: str = "data/voice_channels.json"):
|
||||
"""
|
||||
Initialize the voice channel tracker.
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ from pydantic import Field
|
||||
|
||||
from models.base import SBABaseModel
|
||||
from models.division import Division
|
||||
from models.manager import Manager
|
||||
|
||||
|
||||
class RosterType(Enum):
|
||||
@ -34,7 +35,9 @@ class Team(SBABaseModel):
|
||||
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")
|
||||
@ -196,5 +199,18 @@ class Team(SBABaseModel):
|
||||
"""
|
||||
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 __str__(self):
|
||||
return f"{self.abbrev} - {self.lname}"
|
||||
@ -37,7 +37,7 @@ class Chart:
|
||||
class ChartService:
|
||||
"""Service for managing gameplay charts and infographics."""
|
||||
|
||||
CHARTS_FILE = Path(__file__).parent.parent / 'storage' / 'charts.json'
|
||||
CHARTS_FILE = Path(__file__).parent.parent / 'data' / 'charts.json'
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the chart service."""
|
||||
@ -235,6 +235,68 @@ class ChartService:
|
||||
self._save_charts()
|
||||
logger.info(f"Removed chart: {key}")
|
||||
|
||||
def add_category(self, key: str, display_name: str) -> None:
|
||||
"""
|
||||
Add a new category.
|
||||
|
||||
Args:
|
||||
key: Unique identifier for the category (e.g., 'gameplay')
|
||||
display_name: Display name for the category (e.g., 'Gameplay Charts')
|
||||
|
||||
Raises:
|
||||
BotException: If category key already exists
|
||||
"""
|
||||
if key in self._categories:
|
||||
raise BotException(f"Category '{key}' already exists")
|
||||
|
||||
self._categories[key] = display_name
|
||||
self._save_charts()
|
||||
logger.info(f"Added category: {key} - {display_name}")
|
||||
|
||||
def remove_category(self, key: str) -> None:
|
||||
"""
|
||||
Remove a category.
|
||||
|
||||
Args:
|
||||
key: Category key to remove
|
||||
|
||||
Raises:
|
||||
BotException: If category doesn't exist or charts are using it
|
||||
"""
|
||||
if key not in self._categories:
|
||||
raise BotException(f"Category '{key}' not found")
|
||||
|
||||
# Check if any charts use this category
|
||||
charts_using = [c for c in self._charts.values() if c.category == key]
|
||||
if charts_using:
|
||||
chart_names = ", ".join([c.name for c in charts_using])
|
||||
raise BotException(
|
||||
f"Cannot remove category '{key}' - used by {len(charts_using)} chart(s): {chart_names}"
|
||||
)
|
||||
|
||||
del self._categories[key]
|
||||
self._save_charts()
|
||||
logger.info(f"Removed category: {key}")
|
||||
|
||||
def update_category(self, key: str, display_name: str) -> None:
|
||||
"""
|
||||
Update category display name.
|
||||
|
||||
Args:
|
||||
key: Category key to update
|
||||
display_name: New display name
|
||||
|
||||
Raises:
|
||||
BotException: If category doesn't exist
|
||||
"""
|
||||
if key not in self._categories:
|
||||
raise BotException(f"Category '{key}' not found")
|
||||
|
||||
old_name = self._categories[key]
|
||||
self._categories[key] = display_name
|
||||
self._save_charts()
|
||||
logger.info(f"Updated category: {key} from '{old_name}' to '{display_name}'")
|
||||
|
||||
def reload_charts(self) -> None:
|
||||
"""Reload charts from the JSON file."""
|
||||
self._load_charts()
|
||||
|
||||
@ -8,7 +8,10 @@ from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from discord import app_commands
|
||||
|
||||
from commands.utilities.charts import ChartCommands, ChartAdminCommands
|
||||
from commands.utilities.charts import (
|
||||
ChartCommands, ChartManageGroup, ChartCategoryGroup,
|
||||
chart_autocomplete, category_autocomplete
|
||||
)
|
||||
from services.chart_service import ChartService, Chart, get_chart_service
|
||||
from exceptions import BotException
|
||||
|
||||
@ -191,6 +194,48 @@ class TestChartService:
|
||||
with pytest.raises(BotException, match="not found"):
|
||||
chart_service.remove_chart('nonexistent')
|
||||
|
||||
def test_add_category(self, chart_service):
|
||||
"""Test adding a new category."""
|
||||
chart_service.add_category(key='stats', display_name='Statistics Charts')
|
||||
|
||||
categories = chart_service.get_categories()
|
||||
assert 'stats' in categories
|
||||
assert categories['stats'] == 'Statistics Charts'
|
||||
|
||||
def test_add_duplicate_category(self, chart_service):
|
||||
"""Test adding a duplicate category raises exception."""
|
||||
with pytest.raises(BotException, match="already exists"):
|
||||
chart_service.add_category(key='gameplay', display_name='Duplicate')
|
||||
|
||||
def test_remove_category(self, chart_service):
|
||||
"""Test removing an unused category."""
|
||||
chart_service.remove_category('reference')
|
||||
|
||||
categories = chart_service.get_categories()
|
||||
assert 'reference' not in categories
|
||||
|
||||
def test_remove_nonexistent_category(self, chart_service):
|
||||
"""Test removing a non-existent category raises exception."""
|
||||
with pytest.raises(BotException, match="not found"):
|
||||
chart_service.remove_category('nonexistent')
|
||||
|
||||
def test_remove_category_with_charts(self, chart_service):
|
||||
"""Test removing a category that charts are using raises exception."""
|
||||
with pytest.raises(BotException, match="Cannot remove category"):
|
||||
chart_service.remove_category('gameplay')
|
||||
|
||||
def test_update_category(self, chart_service):
|
||||
"""Test updating a category display name."""
|
||||
chart_service.update_category(key='gameplay', display_name='Updated Gameplay')
|
||||
|
||||
categories = chart_service.get_categories()
|
||||
assert categories['gameplay'] == 'Updated Gameplay'
|
||||
|
||||
def test_update_nonexistent_category(self, chart_service):
|
||||
"""Test updating a non-existent category raises exception."""
|
||||
with pytest.raises(BotException, match="not found"):
|
||||
chart_service.update_category(key='nonexistent', display_name='New Name')
|
||||
|
||||
|
||||
class TestChartCommands:
|
||||
"""Tests for ChartCommands class."""
|
||||
@ -244,40 +289,47 @@ class TestChartCommands:
|
||||
await chart_cog.charts.callback(chart_cog, mock_interaction, 'nonexistent')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_autocomplete(self, chart_cog, mock_interaction):
|
||||
async def test_chart_autocomplete(self, chart_service, mock_interaction):
|
||||
"""Test chart autocomplete functionality."""
|
||||
# Patch get_chart_service to return our test service
|
||||
with patch('commands.utilities.charts.get_chart_service', return_value=chart_service):
|
||||
# Test with empty current input
|
||||
choices = await chart_cog.chart_autocomplete(mock_interaction, '')
|
||||
choices = await chart_autocomplete(mock_interaction, '')
|
||||
assert len(choices) == 3
|
||||
|
||||
# Test with partial match
|
||||
choices = await chart_cog.chart_autocomplete(mock_interaction, 'def')
|
||||
choices = await chart_autocomplete(mock_interaction, 'def')
|
||||
assert len(choices) == 1
|
||||
assert choices[0].value == 'defense'
|
||||
|
||||
# Test with no match
|
||||
choices = await chart_cog.chart_autocomplete(mock_interaction, 'xyz')
|
||||
choices = await chart_autocomplete(mock_interaction, 'xyz')
|
||||
assert len(choices) == 0
|
||||
|
||||
|
||||
class TestChartAdminCommands:
|
||||
"""Tests for ChartAdminCommands class."""
|
||||
class TestChartManageGroup:
|
||||
"""Tests for ChartManageGroup command group."""
|
||||
|
||||
@pytest.fixture
|
||||
def admin_cog(self, chart_service):
|
||||
"""Create ChartAdminCommands cog with mocked service."""
|
||||
bot = AsyncMock()
|
||||
cog = ChartAdminCommands(bot)
|
||||
def manage_group(self, chart_service):
|
||||
"""Create ChartManageGroup with mocked service."""
|
||||
group = ChartManageGroup()
|
||||
|
||||
with patch.object(cog, 'chart_service', chart_service):
|
||||
yield cog
|
||||
with patch.object(group, 'chart_service', chart_service):
|
||||
yield group
|
||||
|
||||
@pytest.fixture
|
||||
def mock_admin_interaction(self, mock_interaction):
|
||||
"""Create a mock interaction with admin permissions."""
|
||||
mock_interaction.user.guild_permissions.administrator = True
|
||||
return mock_interaction
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_add_command(self, admin_cog, mock_interaction):
|
||||
async def test_chart_add_command(self, manage_group, mock_admin_interaction):
|
||||
"""Test adding a new chart via command."""
|
||||
await admin_cog.chart_add.callback(
|
||||
admin_cog,
|
||||
mock_interaction,
|
||||
await manage_group.add.callback(
|
||||
manage_group,
|
||||
mock_admin_interaction,
|
||||
key='new-chart',
|
||||
name='New Chart',
|
||||
category='gameplay',
|
||||
@ -286,25 +338,25 @@ class TestChartAdminCommands:
|
||||
)
|
||||
|
||||
# Verify success response
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Chart Added' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
# Verify chart was added
|
||||
chart = admin_cog.chart_service.get_chart('new-chart')
|
||||
chart = manage_group.chart_service.get_chart('new-chart')
|
||||
assert chart is not None
|
||||
assert chart.name == 'New Chart'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_add_invalid_category(self, admin_cog, mock_interaction):
|
||||
async def test_chart_add_invalid_category(self, manage_group, mock_admin_interaction):
|
||||
"""Test adding a chart with invalid category."""
|
||||
with pytest.raises(BotException, match="Invalid category"):
|
||||
await admin_cog.chart_add.callback(
|
||||
admin_cog,
|
||||
mock_interaction,
|
||||
await manage_group.add.callback(
|
||||
manage_group,
|
||||
mock_admin_interaction,
|
||||
key='new-chart',
|
||||
name='New Chart',
|
||||
category='invalid-category',
|
||||
@ -313,60 +365,34 @@ class TestChartAdminCommands:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_remove_command(self, admin_cog, mock_interaction):
|
||||
async def test_chart_remove_command(self, manage_group, mock_admin_interaction):
|
||||
"""Test removing a chart via command."""
|
||||
await admin_cog.chart_remove.callback(admin_cog, mock_interaction, 'rest')
|
||||
await manage_group.remove.callback(manage_group, mock_admin_interaction, 'rest')
|
||||
|
||||
# Verify success response
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Chart Removed' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
# Verify chart was removed
|
||||
chart = admin_cog.chart_service.get_chart('rest')
|
||||
chart = manage_group.chart_service.get_chart('rest')
|
||||
assert chart is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_remove_not_found(self, admin_cog, mock_interaction):
|
||||
async def test_chart_remove_not_found(self, manage_group, mock_admin_interaction):
|
||||
"""Test removing a non-existent chart."""
|
||||
with pytest.raises(BotException, match="not found"):
|
||||
await admin_cog.chart_remove.callback(admin_cog, mock_interaction, 'nonexistent')
|
||||
await manage_group.remove.callback(manage_group, mock_admin_interaction, 'nonexistent')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_list_all(self, admin_cog, mock_interaction):
|
||||
"""Test listing all charts."""
|
||||
await admin_cog.chart_list.callback(admin_cog, mock_interaction, category=None)
|
||||
|
||||
# Verify response
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '📊 All Available Charts' in embed.title
|
||||
assert 'Total: 3 chart(s)' in embed.description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_list_by_category(self, admin_cog, mock_interaction):
|
||||
"""Test listing charts by category."""
|
||||
await admin_cog.chart_list.callback(admin_cog, mock_interaction, category='gameplay')
|
||||
|
||||
# Verify response
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert "Charts in 'gameplay'" in embed.title
|
||||
assert 'Total: 2 chart(s)' in embed.description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_update_command(self, admin_cog, mock_interaction):
|
||||
async def test_chart_update_command(self, manage_group, mock_admin_interaction):
|
||||
"""Test updating a chart via command."""
|
||||
await admin_cog.chart_update.callback(
|
||||
admin_cog,
|
||||
mock_interaction,
|
||||
await manage_group.update.callback(
|
||||
manage_group,
|
||||
mock_admin_interaction,
|
||||
key='rest',
|
||||
name='Updated Rest Chart',
|
||||
category=None,
|
||||
@ -375,24 +401,24 @@ class TestChartAdminCommands:
|
||||
)
|
||||
|
||||
# Verify success response
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Chart Updated' in embed.title
|
||||
|
||||
# Verify chart was updated
|
||||
chart = admin_cog.chart_service.get_chart('rest')
|
||||
chart = manage_group.chart_service.get_chart('rest')
|
||||
assert chart.name == 'Updated Rest Chart'
|
||||
assert chart.description == 'Updated description'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_update_no_fields(self, admin_cog, mock_interaction):
|
||||
async def test_chart_update_no_fields(self, manage_group, mock_admin_interaction):
|
||||
"""Test updating with no fields raises exception."""
|
||||
with pytest.raises(BotException, match="Must provide at least one field"):
|
||||
await admin_cog.chart_update.callback(
|
||||
admin_cog,
|
||||
mock_interaction,
|
||||
await manage_group.update.callback(
|
||||
manage_group,
|
||||
mock_admin_interaction,
|
||||
key='rest',
|
||||
name=None,
|
||||
category=None,
|
||||
@ -401,15 +427,114 @@ class TestChartAdminCommands:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chart_update_invalid_category(self, admin_cog, mock_interaction):
|
||||
async def test_chart_update_invalid_category(self, manage_group, mock_admin_interaction):
|
||||
"""Test updating with invalid category."""
|
||||
with pytest.raises(BotException, match="Invalid category"):
|
||||
await admin_cog.chart_update.callback(
|
||||
admin_cog,
|
||||
mock_interaction,
|
||||
await manage_group.update.callback(
|
||||
manage_group,
|
||||
mock_admin_interaction,
|
||||
key='rest',
|
||||
name=None,
|
||||
category='invalid-category',
|
||||
url=None,
|
||||
description=None
|
||||
)
|
||||
|
||||
|
||||
class TestChartCategoryGroup:
|
||||
"""Tests for ChartCategoryGroup command group."""
|
||||
|
||||
@pytest.fixture
|
||||
def category_group(self, chart_service):
|
||||
"""Create ChartCategoryGroup with mocked service."""
|
||||
group = ChartCategoryGroup()
|
||||
|
||||
with patch.object(group, 'chart_service', chart_service):
|
||||
yield group
|
||||
|
||||
@pytest.fixture
|
||||
def mock_admin_interaction(self, mock_interaction):
|
||||
"""Create a mock interaction with admin permissions."""
|
||||
mock_interaction.user.guild_permissions.administrator = True
|
||||
return mock_interaction
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_categories(self, category_group, mock_admin_interaction):
|
||||
"""Test listing all categories."""
|
||||
await category_group.list_categories.callback(
|
||||
category_group,
|
||||
mock_admin_interaction
|
||||
)
|
||||
|
||||
# Verify response
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '📊 Chart Categories' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_category(self, category_group, mock_admin_interaction):
|
||||
"""Test adding a new category."""
|
||||
await category_group.add_category.callback(
|
||||
category_group,
|
||||
mock_admin_interaction,
|
||||
key='stats',
|
||||
display_name='Statistics Charts'
|
||||
)
|
||||
|
||||
# Verify success response
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Category Added' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
# Verify category was added
|
||||
categories = category_group.chart_service.get_categories()
|
||||
assert 'stats' in categories
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_category(self, category_group, mock_admin_interaction):
|
||||
"""Test removing a category."""
|
||||
await category_group.remove_category.callback(
|
||||
category_group,
|
||||
mock_admin_interaction,
|
||||
key='reference'
|
||||
)
|
||||
|
||||
# Verify success response
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Category Removed' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
# Verify category was removed
|
||||
categories = category_group.chart_service.get_categories()
|
||||
assert 'reference' not in categories
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_category(self, category_group, mock_admin_interaction):
|
||||
"""Test renaming a category."""
|
||||
await category_group.rename_category.callback(
|
||||
category_group,
|
||||
mock_admin_interaction,
|
||||
key='gameplay',
|
||||
new_display_name='Updated Gameplay'
|
||||
)
|
||||
|
||||
# Verify success response
|
||||
mock_admin_interaction.response.send_message.assert_called_once()
|
||||
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
||||
embed = call_kwargs['embed']
|
||||
|
||||
assert '✅ Category Renamed' in embed.title
|
||||
assert call_kwargs['ephemeral'] is True
|
||||
|
||||
# Verify category was renamed
|
||||
categories = category_group.chart_service.get_categories()
|
||||
assert categories['gameplay'] == 'Updated Gameplay'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user