CLAUDE: Add pagination and move ID display to /mymoves command
- Fix transaction count bug by filtering transactions >= current week - Add TransactionPaginationView with "Show Move IDs" button - Implement intelligent message chunking for long transaction lists - Remove emojis from individual transaction lines (kept in headers) - Display 10 transactions per page with navigation buttons - Show move IDs on demand via ephemeral button (stays under 2000 char limit) - Update all tests to validate pagination and chunking behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b61cad2478
commit
3c66ada99d
@ -12,7 +12,9 @@ from discord import app_commands
|
||||
|
||||
from utils.logging import get_contextual_logger
|
||||
from utils.decorators import logged_command
|
||||
from utils.team_utils import get_user_major_league_team
|
||||
from views.embeds import EmbedColors, EmbedTemplate
|
||||
from views.base import PaginationView
|
||||
from constants import SBA_CURRENT_SEASON
|
||||
|
||||
from services.transaction_service import transaction_service
|
||||
@ -21,6 +23,81 @@ from services.team_service import team_service
|
||||
# No longer need TransactionStatus enum
|
||||
|
||||
|
||||
class TransactionPaginationView(PaginationView):
|
||||
"""Custom pagination view with Show Move IDs button."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pages: list[discord.Embed],
|
||||
all_transactions: list,
|
||||
user_id: int,
|
||||
timeout: float = 300.0,
|
||||
show_page_numbers: bool = True
|
||||
):
|
||||
super().__init__(
|
||||
pages=pages,
|
||||
user_id=user_id,
|
||||
timeout=timeout,
|
||||
show_page_numbers=show_page_numbers
|
||||
)
|
||||
self.all_transactions = all_transactions
|
||||
|
||||
@discord.ui.button(label="Show Move IDs", style=discord.ButtonStyle.secondary, emoji="🔍", row=1)
|
||||
async def show_move_ids(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
"""Show all move IDs in an ephemeral message."""
|
||||
self.increment_interaction_count()
|
||||
|
||||
if not self.all_transactions:
|
||||
await interaction.response.send_message(
|
||||
"No transactions to show.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Build the move ID list
|
||||
header = "📋 **Move IDs for Your Transactions**\n"
|
||||
lines = []
|
||||
|
||||
for transaction in self.all_transactions:
|
||||
lines.append(
|
||||
f"• Week {transaction.week}: {transaction.player.name} → `{transaction.moveid}`"
|
||||
)
|
||||
|
||||
# Discord has a 2000 character limit for messages
|
||||
# Chunk messages to stay under the limit
|
||||
messages = []
|
||||
current_message = header
|
||||
|
||||
for line in lines:
|
||||
# Check if adding this line would exceed limit (leave 50 char buffer)
|
||||
if len(current_message) + len(line) + 1 > 1950:
|
||||
messages.append(current_message)
|
||||
current_message = line + "\n"
|
||||
else:
|
||||
current_message += line + "\n"
|
||||
|
||||
# Add the last message if it has content beyond the header
|
||||
if current_message.strip() != header.strip():
|
||||
messages.append(current_message)
|
||||
|
||||
# Send the messages
|
||||
if not messages:
|
||||
await interaction.response.send_message(
|
||||
"No transactions to display.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Send first message as response
|
||||
await interaction.response.send_message(messages[0], ephemeral=True)
|
||||
|
||||
# Send remaining messages as followups
|
||||
if len(messages) > 1:
|
||||
for msg in messages[1:]:
|
||||
await interaction.followup.send(msg, ephemeral=True)
|
||||
|
||||
|
||||
class TransactionCommands(commands.Cog):
|
||||
"""Transaction command handlers for roster management."""
|
||||
|
||||
@ -45,17 +122,15 @@ class TransactionCommands(commands.Cog):
|
||||
await interaction.response.defer()
|
||||
|
||||
# Get user's team
|
||||
user_teams = await team_service.get_teams_by_owner(interaction.user.id, SBA_CURRENT_SEASON)
|
||||
team = await get_user_major_league_team(interaction.user.id, SBA_CURRENT_SEASON)
|
||||
|
||||
if not user_teams:
|
||||
if not team:
|
||||
await interaction.followup.send(
|
||||
"❌ You don't appear to own a team in the current season.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
team = user_teams[0] # Use first team if multiple
|
||||
|
||||
# Get transactions in parallel
|
||||
pending_task = transaction_service.get_pending_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
||||
frozen_task = transaction_service.get_frozen_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
||||
@ -69,20 +144,40 @@ class TransactionCommands(commands.Cog):
|
||||
cancelled_transactions = []
|
||||
if show_cancelled:
|
||||
cancelled_transactions = await transaction_service.get_team_transactions(
|
||||
team.abbrev,
|
||||
team.abbrev,
|
||||
SBA_CURRENT_SEASON,
|
||||
cancelled=True
|
||||
)
|
||||
|
||||
embed = await self._create_my_moves_embed(
|
||||
team,
|
||||
pending_transactions,
|
||||
frozen_transactions,
|
||||
|
||||
pages = self._create_my_moves_pages(
|
||||
team,
|
||||
pending_transactions,
|
||||
frozen_transactions,
|
||||
processed_transactions,
|
||||
cancelled_transactions
|
||||
)
|
||||
|
||||
await interaction.followup.send(embed=embed)
|
||||
|
||||
# Collect all transactions for the "Show Move IDs" button
|
||||
all_transactions = (
|
||||
pending_transactions +
|
||||
frozen_transactions +
|
||||
processed_transactions +
|
||||
cancelled_transactions
|
||||
)
|
||||
|
||||
# If only one page and no transactions, send without any buttons
|
||||
if len(pages) == 1 and not all_transactions:
|
||||
await interaction.followup.send(embed=pages[0])
|
||||
else:
|
||||
# Use custom pagination view with "Show Move IDs" button
|
||||
view = TransactionPaginationView(
|
||||
pages=pages,
|
||||
all_transactions=all_transactions,
|
||||
user_id=interaction.user.id,
|
||||
timeout=300.0,
|
||||
show_page_numbers=True
|
||||
)
|
||||
await interaction.followup.send(embed=view.get_current_embed(), view=view)
|
||||
|
||||
@app_commands.command(
|
||||
name="legal",
|
||||
@ -159,106 +254,166 @@ class TransactionCommands(commands.Cog):
|
||||
|
||||
await interaction.followup.send(embed=embed)
|
||||
|
||||
async def _create_my_moves_embed(
|
||||
def _create_my_moves_pages(
|
||||
self,
|
||||
team,
|
||||
pending_transactions,
|
||||
frozen_transactions,
|
||||
processed_transactions,
|
||||
cancelled_transactions
|
||||
) -> discord.Embed:
|
||||
"""Create embed showing user's transaction status."""
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
# Add team thumbnail if available
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
# Pending transactions
|
||||
) -> list[discord.Embed]:
|
||||
"""Create paginated embeds showing user's transaction status."""
|
||||
|
||||
pages = []
|
||||
transactions_per_page = 10
|
||||
|
||||
# Helper function to create transaction lines without emojis
|
||||
def format_transaction(transaction):
|
||||
return f"Week {transaction.week}: {transaction.move_description}"
|
||||
|
||||
# Page 1: Summary + Pending Transactions
|
||||
if pending_transactions:
|
||||
pending_lines = []
|
||||
for transaction in pending_transactions[-5:]: # Show last 5
|
||||
pending_lines.append(
|
||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
||||
total_pending = len(pending_transactions)
|
||||
total_pages = (total_pending + transactions_per_page - 1) // transactions_per_page
|
||||
|
||||
for page_num in range(total_pages):
|
||||
start_idx = page_num * transactions_per_page
|
||||
end_idx = min(start_idx + transactions_per_page, total_pending)
|
||||
page_transactions = pending_transactions[start_idx:end_idx]
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
|
||||
# Add team thumbnail if available
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
# Pending transactions for this page
|
||||
pending_lines = [format_transaction(tx) for tx in page_transactions]
|
||||
|
||||
embed.add_field(
|
||||
name=f"⏳ Pending Transactions ({total_pending} total)",
|
||||
value="\n".join(pending_lines),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Add summary only on first page
|
||||
if page_num == 0:
|
||||
total_frozen = len(frozen_transactions)
|
||||
status_text = []
|
||||
if total_pending > 0:
|
||||
status_text.append(f"{total_pending} pending")
|
||||
if total_frozen > 0:
|
||||
status_text.append(f"{total_frozen} scheduled")
|
||||
|
||||
embed.add_field(
|
||||
name="Summary",
|
||||
value=", ".join(status_text) if status_text else "No active transactions",
|
||||
inline=True
|
||||
)
|
||||
|
||||
pages.append(embed)
|
||||
else:
|
||||
# No pending transactions - create single page
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
embed.add_field(
|
||||
name="⏳ Pending Transactions",
|
||||
value="\n".join(pending_lines),
|
||||
inline=False
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name="⏳ Pending Transactions",
|
||||
value="No pending transactions",
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Frozen transactions (scheduled for processing)
|
||||
if frozen_transactions:
|
||||
frozen_lines = []
|
||||
for transaction in frozen_transactions[-3:]: # Show last 3
|
||||
frozen_lines.append(
|
||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
||||
)
|
||||
|
||||
|
||||
total_frozen = len(frozen_transactions)
|
||||
status_text = []
|
||||
if total_frozen > 0:
|
||||
status_text.append(f"{total_frozen} scheduled")
|
||||
|
||||
embed.add_field(
|
||||
name="❄️ Scheduled for Processing",
|
||||
name="Summary",
|
||||
value=", ".join(status_text) if status_text else "No active transactions",
|
||||
inline=True
|
||||
)
|
||||
|
||||
pages.append(embed)
|
||||
|
||||
# Additional page: Frozen transactions
|
||||
if frozen_transactions:
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
frozen_lines = [format_transaction(tx) for tx in frozen_transactions]
|
||||
|
||||
embed.add_field(
|
||||
name=f"❄️ Scheduled for Processing ({len(frozen_transactions)} total)",
|
||||
value="\n".join(frozen_lines),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Recent processed transactions
|
||||
|
||||
pages.append(embed)
|
||||
|
||||
# Additional page: Recently processed transactions
|
||||
if processed_transactions:
|
||||
processed_lines = []
|
||||
for transaction in processed_transactions[-3:]: # Show last 3
|
||||
processed_lines.append(
|
||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
||||
)
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
processed_lines = [format_transaction(tx) for tx in processed_transactions[-20:]] # Last 20
|
||||
|
||||
embed.add_field(
|
||||
name="✅ Recently Processed",
|
||||
name=f"✅ Recently Processed ({len(processed_transactions[-20:])} shown)",
|
||||
value="\n".join(processed_lines),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Cancelled transactions (if requested)
|
||||
|
||||
pages.append(embed)
|
||||
|
||||
# Additional page: Cancelled transactions (if requested)
|
||||
if cancelled_transactions:
|
||||
cancelled_lines = []
|
||||
for transaction in cancelled_transactions[-2:]: # Show last 2
|
||||
cancelled_lines.append(
|
||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
||||
)
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"📋 Transaction Status - {team.abbrev}",
|
||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||
color=EmbedColors.INFO
|
||||
)
|
||||
|
||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
cancelled_lines = [format_transaction(tx) for tx in cancelled_transactions[-20:]] # Last 20
|
||||
|
||||
embed.add_field(
|
||||
name="❌ Cancelled Transactions",
|
||||
name=f"❌ Cancelled Transactions ({len(cancelled_transactions[-20:])} shown)",
|
||||
value="\n".join(cancelled_lines),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Transaction summary
|
||||
total_pending = len(pending_transactions)
|
||||
total_frozen = len(frozen_transactions)
|
||||
|
||||
status_text = []
|
||||
if total_pending > 0:
|
||||
status_text.append(f"{total_pending} pending")
|
||||
if total_frozen > 0:
|
||||
status_text.append(f"{total_frozen} scheduled")
|
||||
|
||||
embed.add_field(
|
||||
name="Summary",
|
||||
value=", ".join(status_text) if status_text else "No active transactions",
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.set_footer(text="Use /legal to check roster legality")
|
||||
return embed
|
||||
|
||||
pages.append(embed)
|
||||
|
||||
# Add footer to all pages
|
||||
for page in pages:
|
||||
page.set_footer(text="Use /legal to check roster legality")
|
||||
|
||||
return pages
|
||||
|
||||
async def _create_legal_embed(
|
||||
self,
|
||||
|
||||
@ -77,13 +77,30 @@ class TransactionService(BaseService[Transaction]):
|
||||
raise APIException(f"Failed to retrieve transactions: {e}")
|
||||
|
||||
async def get_pending_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
||||
"""Get pending transactions for a team."""
|
||||
return await self.get_team_transactions(
|
||||
team_abbrev,
|
||||
season,
|
||||
cancelled=False,
|
||||
frozen=False
|
||||
)
|
||||
"""Get pending (future) transactions for a team."""
|
||||
try:
|
||||
# Get current week to filter future transactions
|
||||
current_data = await self.get_client()
|
||||
current_response = await current_data.get('current')
|
||||
current_week = current_response.get('week', 0) if current_response else 0
|
||||
|
||||
# Get transactions from current week onward
|
||||
return await self.get_team_transactions(
|
||||
team_abbrev,
|
||||
season,
|
||||
cancelled=False,
|
||||
frozen=False,
|
||||
week_start=current_week
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get current week, returning all non-cancelled/non-frozen transactions: {e}")
|
||||
# Fallback to all non-cancelled/non-frozen if we can't get current week
|
||||
return await self.get_team_transactions(
|
||||
team_abbrev,
|
||||
season,
|
||||
cancelled=False,
|
||||
frozen=False
|
||||
)
|
||||
|
||||
async def get_frozen_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
||||
"""Get frozen (scheduled for processing) transactions for a team."""
|
||||
|
||||
@ -113,31 +113,28 @@ class TestTransactionCommands:
|
||||
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
|
||||
with patch('utils.team_utils.team_service') as mock_team_utils_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
|
||||
# Mock service responses
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_team_utils_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
|
||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=frozen_tx)
|
||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
# Execute command
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||
|
||||
|
||||
# Verify interaction flow
|
||||
mock_interaction.response.defer.assert_called_once()
|
||||
mock_interaction.followup.send.assert_called_once()
|
||||
|
||||
|
||||
# Verify service calls
|
||||
mock_team_service.get_teams_by_owner.assert_called_once_with(
|
||||
mock_interaction.user.id, 12
|
||||
)
|
||||
mock_tx_service.get_pending_transactions.assert_called_once_with('WV', 12)
|
||||
mock_tx_service.get_frozen_transactions.assert_called_once_with('WV', 12)
|
||||
mock_tx_service.get_processed_transactions.assert_called_once_with('WV', 12)
|
||||
|
||||
|
||||
# Check embed was sent
|
||||
embed_call = mock_interaction.followup.send.call_args
|
||||
assert 'embed' in embed_call.kwargs
|
||||
@ -146,18 +143,18 @@ class TestTransactionCommands:
|
||||
async def test_my_moves_with_cancelled(self, commands_cog, mock_interaction, mock_team, mock_transactions):
|
||||
"""Test /mymoves command with cancelled transactions shown."""
|
||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=cancelled_tx)
|
||||
|
||||
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=True)
|
||||
|
||||
|
||||
# Verify cancelled transactions were requested
|
||||
mock_tx_service.get_team_transactions.assert_called_once_with(
|
||||
'WV', 12, cancelled=True
|
||||
@ -166,11 +163,11 @@ class TestTransactionCommands:
|
||||
@pytest.mark.asyncio
|
||||
async def test_my_moves_no_team(self, commands_cog, mock_interaction):
|
||||
"""Test /mymoves command when user has no team."""
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
||||
|
||||
|
||||
# Should send error message
|
||||
mock_interaction.followup.send.assert_called_once()
|
||||
call_args = mock_interaction.followup.send.call_args
|
||||
@ -180,12 +177,12 @@ class TestTransactionCommands:
|
||||
@pytest.mark.asyncio
|
||||
async def test_my_moves_api_error(self, commands_cog, mock_interaction, mock_team):
|
||||
"""Test /mymoves command with API error."""
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions.side_effect = APIException("API Error")
|
||||
|
||||
|
||||
# Should raise the exception (logged_command decorator handles it)
|
||||
with pytest.raises(APIException):
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
||||
@ -308,45 +305,167 @@ class TestTransactionCommands:
|
||||
assert "Could not retrieve roster data" in call_args.args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_my_moves_embed(self, commands_cog, mock_team, mock_transactions):
|
||||
"""Test embed creation for /mymoves command."""
|
||||
async def test_create_my_moves_pages(self, commands_cog, mock_team, mock_transactions):
|
||||
"""Test paginated embed creation for /mymoves command."""
|
||||
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
||||
processed_tx = []
|
||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||
|
||||
embed = await commands_cog._create_my_moves_embed(
|
||||
|
||||
pages = commands_cog._create_my_moves_pages(
|
||||
mock_team, pending_tx, frozen_tx, processed_tx, cancelled_tx
|
||||
)
|
||||
|
||||
assert isinstance(embed, discord.Embed)
|
||||
assert embed.title == "📋 Transaction Status - WV"
|
||||
assert "West Virginia Black Bears • Season 12" in embed.description
|
||||
|
||||
# Check that fields are created for each transaction type
|
||||
field_names = [field.name for field in embed.fields]
|
||||
assert "⏳ Pending Transactions" in field_names
|
||||
assert "❄️ Scheduled for Processing" in field_names
|
||||
assert "❌ Cancelled Transactions" in field_names
|
||||
assert "Summary" in field_names
|
||||
|
||||
|
||||
assert len(pages) > 0
|
||||
first_page = pages[0]
|
||||
assert isinstance(first_page, discord.Embed)
|
||||
assert first_page.title == "📋 Transaction Status - WV"
|
||||
assert "West Virginia Black Bears • Season 12" in first_page.description
|
||||
|
||||
# Check that fields are created for transaction types
|
||||
field_names = [field.name for field in first_page.fields]
|
||||
assert any("Pending Transactions" in name for name in field_names)
|
||||
|
||||
# Verify thumbnail is set
|
||||
assert embed.thumbnail.url == mock_team.thumbnail
|
||||
assert first_page.thumbnail.url == mock_team.thumbnail
|
||||
|
||||
# Verify emoji is NOT in individual transaction lines
|
||||
for page in pages:
|
||||
for field in page.fields:
|
||||
if "Pending" in field.name or "Scheduled" in field.name or "Cancelled" in field.name:
|
||||
# Check that emojis (⏳, ❄️, ❌) are NOT in the field value
|
||||
assert "⏳" not in field.value
|
||||
assert "❄️" not in field.value
|
||||
assert "✅" not in field.value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_my_moves_embed_no_transactions(self, commands_cog, mock_team):
|
||||
"""Test embed creation with no transactions."""
|
||||
embed = await commands_cog._create_my_moves_embed(
|
||||
async def test_create_my_moves_pages_no_transactions(self, commands_cog, mock_team):
|
||||
"""Test paginated embed creation with no transactions."""
|
||||
pages = commands_cog._create_my_moves_pages(
|
||||
mock_team, [], [], [], []
|
||||
)
|
||||
|
||||
|
||||
assert len(pages) == 1 # Should have single page
|
||||
embed = pages[0]
|
||||
|
||||
# Find the pending transactions field
|
||||
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
||||
assert pending_field.value == "No pending transactions"
|
||||
|
||||
|
||||
# Summary should show no active transactions
|
||||
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
||||
assert summary_field.value == "No active transactions"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transaction_pagination_view_with_move_ids(self, commands_cog, mock_interaction, mock_team, mock_transactions):
|
||||
"""Test that TransactionPaginationView is created with move IDs button."""
|
||||
from commands.transactions.management import TransactionPaginationView
|
||||
|
||||
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
|
||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||
|
||||
# Verify TransactionPaginationView was created
|
||||
mock_interaction.followup.send.assert_called_once()
|
||||
call_args = mock_interaction.followup.send.call_args
|
||||
view = call_args.kwargs.get('view')
|
||||
|
||||
assert view is not None
|
||||
assert isinstance(view, TransactionPaginationView)
|
||||
assert len(view.all_transactions) == len(pending_tx)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_show_move_ids_handles_long_lists(self, mock_team, mock_transactions):
|
||||
"""Test that Show Move IDs button properly chunks very long transaction lists."""
|
||||
from commands.transactions.management import TransactionPaginationView
|
||||
|
||||
# Create 100 transactions to simulate a very long list
|
||||
base_data = {
|
||||
'season': 12,
|
||||
'player': {
|
||||
'id': 12472,
|
||||
'name': 'Very Long Player Name That Takes Up Space',
|
||||
'wara': 2.47,
|
||||
'season': 12,
|
||||
'pos_1': 'LF'
|
||||
},
|
||||
'oldteam': {
|
||||
'id': 508,
|
||||
'abbrev': 'NYD',
|
||||
'sname': 'Diamonds',
|
||||
'lname': 'New York Diamonds',
|
||||
'season': 12
|
||||
},
|
||||
'newteam': {
|
||||
'id': 499,
|
||||
'abbrev': 'WV',
|
||||
'sname': 'Black Bears',
|
||||
'lname': 'West Virginia Black Bears',
|
||||
'season': 12
|
||||
}
|
||||
}
|
||||
|
||||
many_transactions = []
|
||||
for i in range(100):
|
||||
tx_data = {
|
||||
**base_data,
|
||||
'id': i,
|
||||
'week': 10 + (i % 5),
|
||||
'moveid': f'Season-012-Week-{10 + (i % 5)}-Move-{i:03d}',
|
||||
'cancelled': False,
|
||||
'frozen': False
|
||||
}
|
||||
many_transactions.append(Transaction.from_api_data(tx_data))
|
||||
|
||||
# Create view with many transactions
|
||||
pages = [discord.Embed(title="Test")]
|
||||
view = TransactionPaginationView(
|
||||
pages=pages,
|
||||
all_transactions=many_transactions,
|
||||
user_id=258104532423147520,
|
||||
timeout=300.0,
|
||||
show_page_numbers=True
|
||||
)
|
||||
|
||||
# Create mock interaction
|
||||
mock_interaction = AsyncMock()
|
||||
mock_button = MagicMock()
|
||||
|
||||
# Find the show_move_ids button and call its callback directly
|
||||
show_move_ids_button = None
|
||||
for item in view.children:
|
||||
if hasattr(item, 'label') and item.label == "Show Move IDs":
|
||||
show_move_ids_button = item
|
||||
break
|
||||
|
||||
assert show_move_ids_button is not None, "Show Move IDs button not found"
|
||||
|
||||
# Call the button's callback
|
||||
await show_move_ids_button.callback(mock_interaction)
|
||||
|
||||
# Verify response was sent
|
||||
mock_interaction.response.send_message.assert_called_once()
|
||||
|
||||
# Get the message that was sent
|
||||
call_args = mock_interaction.response.send_message.call_args
|
||||
first_message = call_args.args[0]
|
||||
|
||||
# Verify first message is under 2000 characters
|
||||
assert len(first_message) < 2000, f"First message is {len(first_message)} characters (exceeds 2000 limit)"
|
||||
|
||||
# If there were followup messages, verify they're also under 2000 chars
|
||||
if mock_interaction.followup.send.called:
|
||||
for call in mock_interaction.followup.send.call_args_list:
|
||||
followup_message = call.args[0]
|
||||
assert len(followup_message) < 2000, f"Followup message is {len(followup_message)} characters (exceeds 2000 limit)"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_legal_embed_all_legal(self, commands_cog, mock_team):
|
||||
@ -470,7 +589,7 @@ class TestTransactionCommandsIntegration:
|
||||
"""Test complete /mymoves workflow with realistic data volumes."""
|
||||
mock_interaction = AsyncMock()
|
||||
mock_interaction.user.id = 258104532423147520
|
||||
|
||||
|
||||
# Create realistic transaction volumes
|
||||
pending_transactions = []
|
||||
for i in range(15): # 15 pending transactions
|
||||
@ -486,7 +605,7 @@ class TestTransactionCommandsIntegration:
|
||||
'frozen': False
|
||||
}
|
||||
pending_transactions.append(Transaction.from_api_data(tx_data))
|
||||
|
||||
|
||||
mock_team = Team.from_api_data({
|
||||
'id': 499,
|
||||
'abbrev': 'WV',
|
||||
@ -494,43 +613,50 @@ class TestTransactionCommandsIntegration:
|
||||
'lname': 'West Virginia Black Bears',
|
||||
'season': 12
|
||||
})
|
||||
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_transactions)
|
||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||
|
||||
|
||||
# Verify embed was created and sent
|
||||
mock_interaction.followup.send.assert_called_once()
|
||||
embed_call = mock_interaction.followup.send.call_args
|
||||
embed = embed_call.kwargs['embed']
|
||||
|
||||
# Check that only last 5 pending transactions are shown
|
||||
|
||||
# With 15 transactions, should show 10 per page
|
||||
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
||||
lines = pending_field.value.split('\n')
|
||||
assert len(lines) == 5 # Should show only last 5
|
||||
|
||||
assert len(lines) == 10 # Should show 10 per page
|
||||
|
||||
# Verify summary shows correct count
|
||||
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
||||
assert "15 pending" in summary_field.value
|
||||
|
||||
# Verify pagination view was created
|
||||
from commands.transactions.management import TransactionPaginationView
|
||||
view = embed_call.kwargs.get('view')
|
||||
assert view is not None
|
||||
assert isinstance(view, TransactionPaginationView)
|
||||
assert len(view.all_transactions) == 15
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_command_execution(self, commands_cog):
|
||||
"""Test that commands can handle concurrent execution."""
|
||||
import asyncio
|
||||
|
||||
|
||||
# Create multiple mock interactions
|
||||
interactions = []
|
||||
for i in range(5):
|
||||
mock_interaction = AsyncMock()
|
||||
mock_interaction.user.id = 258104532423147520 + i
|
||||
interactions.append(mock_interaction)
|
||||
|
||||
|
||||
mock_team = Team.from_api_data({
|
||||
'id': 499,
|
||||
'abbrev': 'WV',
|
||||
@ -538,22 +664,22 @@ class TestTransactionCommandsIntegration:
|
||||
'lname': 'West Virginia Black Bears',
|
||||
'season': 12
|
||||
})
|
||||
|
||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
||||
|
||||
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||
|
||||
|
||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
# Execute commands concurrently
|
||||
tasks = [commands_cog.my_moves.callback(commands_cog, interaction) for interaction in interactions]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
# All should complete successfully
|
||||
assert len([r for r in results if not isinstance(r, Exception)]) == 5
|
||||
|
||||
|
||||
# All interactions should have received responses
|
||||
for interaction in interactions:
|
||||
interaction.followup.send.assert_called_once()
|
||||
Loading…
Reference in New Issue
Block a user