major-domo-v2/models/help_command.py
Cal Corum bcd6a10aef CLAUDE: Implement custom help commands system
Add comprehensive admin-managed help system for league documentation,
resources, FAQs, and guides. Replaces planned /links command with a
more flexible and powerful solution.

Features:
- Full CRUD operations via Discord commands (/help, /help-create, /help-edit, /help-delete, /help-list)
- Permission-based access control (admins + Help Editor role)
- Markdown-formatted content with category organization
- View tracking and analytics
- Soft delete with restore capability
- Full audit trail (creator, editor, timestamps)
- Autocomplete for topic discovery
- Interactive modals and paginated list views

Implementation:
- New models/help_command.py with Pydantic validation
- New services/help_commands_service.py with full CRUD API integration
- New views/help_commands.py with interactive modals and views
- New commands/help/ package with command handlers
- Comprehensive README.md documentation in commands/help/
- Test coverage for models and services

Configuration:
- Added HELP_EDITOR_ROLE_NAME constant to constants.py
- Updated bot.py to load help commands
- Updated PRE_LAUNCH_ROADMAP.md to mark system as complete
- Updated CLAUDE.md documentation

Requires database migration for help_commands table.
See .claude/DATABASE_MIGRATION_HELP_COMMANDS.md for details.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 13:14:13 -05:00

228 lines
7.5 KiB
Python

"""
Help Command models for Discord Bot v2.0
Modern Pydantic models for the custom help system with full type safety.
Allows admins and help editors to create custom help topics for league documentation,
resources, FAQs, links, and guides.
"""
from datetime import datetime
from typing import Optional
import re
from pydantic import BaseModel, Field, field_validator
from models.base import SBABaseModel
class HelpCommand(SBABaseModel):
"""A help topic created by an admin or help editor."""
id: int = Field(..., description="Database ID") # type: ignore
name: str = Field(..., description="Help topic name (unique)")
title: str = Field(..., description="Display title")
content: str = Field(..., description="Help content (markdown supported)")
category: Optional[str] = Field(None, description="Category for organization")
# Audit fields
created_by_discord_id: str = Field(..., description="Creator Discord ID (stored as text)")
created_at: datetime = Field(..., description="When help topic was created") # type: ignore
updated_at: Optional[datetime] = Field(None, description="When help topic was last updated") # type: ignore
last_modified_by: Optional[str] = Field(None, description="Discord ID of last editor (stored as text)")
# Status and metrics
is_active: bool = Field(True, description="Whether help topic is active (soft delete)")
view_count: int = Field(0, description="Number of times viewed")
display_order: int = Field(0, description="Sort order for display")
@field_validator('name')
@classmethod
def validate_name(cls, v):
"""Validate help topic name."""
if not v or len(v.strip()) == 0:
raise ValueError("Help topic name cannot be empty")
name = v.strip().lower()
# Length validation
if len(name) < 2:
raise ValueError("Help topic name must be at least 2 characters")
if len(name) > 32:
raise ValueError("Help topic name cannot exceed 32 characters")
# Character validation - only allow alphanumeric, dashes, underscores
if not re.match(r'^[a-z0-9_-]+$', name):
raise ValueError("Help topic name can only contain letters, numbers, dashes, and underscores")
return name.lower()
@field_validator('title')
@classmethod
def validate_title(cls, v):
"""Validate help topic title."""
if not v or len(v.strip()) == 0:
raise ValueError("Help topic title cannot be empty")
title = v.strip()
# Length validation
if len(title) > 200:
raise ValueError("Help topic title cannot exceed 200 characters")
return title
@field_validator('content')
@classmethod
def validate_content(cls, v):
"""Validate help topic content."""
if not v or len(v.strip()) == 0:
raise ValueError("Help topic content cannot be empty")
content = v.strip()
# Length validation
if len(content) > 4000:
raise ValueError("Help topic content cannot exceed 4000 characters")
# Basic content filtering (still allow @mentions in help content)
# We allow @everyone and @here in help content since it's admin-controlled
return content
@field_validator('category')
@classmethod
def validate_category(cls, v):
"""Validate category if provided."""
if v is None:
return v
category = v.strip().lower()
if len(category) == 0:
return None # Empty string becomes None
# Length validation
if len(category) > 50:
raise ValueError("Category cannot exceed 50 characters")
# Character validation
if not re.match(r'^[a-z0-9_-]+$', category):
raise ValueError("Category can only contain letters, numbers, dashes, and underscores")
return category
@property
def is_deleted(self) -> bool:
"""Check if help topic is soft deleted."""
return not self.is_active
@property
def days_since_update(self) -> Optional[int]:
"""Calculate days since last update."""
if not self.updated_at:
return None
return (datetime.now() - self.updated_at).days
@property
def days_since_creation(self) -> int:
"""Calculate days since creation."""
return (datetime.now() - self.created_at).days
@property
def popularity_score(self) -> float:
"""
Calculate popularity score based on view count and recency.
Higher score = more popular topic.
"""
if self.view_count == 0:
return 0.0
# Base score from views
base_score = min(self.view_count / 10.0, 10.0) # Max 10 points from views
# Recency modifier based on creation date
days_old = self.days_since_creation
if days_old <= 7:
recency_modifier = 1.5 # New topic bonus
elif days_old <= 30:
recency_modifier = 1.2 # Recent bonus
elif days_old <= 90:
recency_modifier = 1.0 # No modifier
else:
recency_modifier = 0.8 # Older topic slight penalty
return base_score * recency_modifier
class HelpCommandSearchFilters(BaseModel):
"""Filters for searching help commands."""
name_contains: Optional[str] = None
category: Optional[str] = None
is_active: bool = True
# Sorting
sort_by: str = Field('name', description="Sort field: name, category, created_at, view_count, display_order")
sort_desc: bool = Field(False, description="Sort in descending order")
# Pagination
page: int = Field(1, description="Page number (1-based)")
page_size: int = Field(25, description="Items per page")
@field_validator('sort_by')
@classmethod
def validate_sort_by(cls, v):
"""Validate sort field."""
valid_sorts = {'name', 'title', 'category', 'created_at', 'updated_at', 'view_count', 'display_order'}
if v not in valid_sorts:
raise ValueError(f"sort_by must be one of: {', '.join(valid_sorts)}")
return v
@field_validator('page')
@classmethod
def validate_page(cls, v):
"""Validate page number."""
if v < 1:
raise ValueError("Page number must be >= 1")
return v
@field_validator('page_size')
@classmethod
def validate_page_size(cls, v):
"""Validate page size."""
if v < 1 or v > 100:
raise ValueError("Page size must be between 1 and 100")
return v
class HelpCommandSearchResult(BaseModel):
"""Result of a help command search."""
help_commands: list[HelpCommand]
total_count: int
page: int
page_size: int
total_pages: int
has_more: bool
@property
def start_index(self) -> int:
"""Get the starting index for this page."""
return (self.page - 1) * self.page_size + 1
@property
def end_index(self) -> int:
"""Get the ending index for this page."""
return min(self.page * self.page_size, self.total_count)
class HelpCommandStats(BaseModel):
"""Statistics about help commands."""
total_commands: int
active_commands: int
total_views: int
most_viewed_command: Optional[HelpCommand] = None
recent_commands_count: int = 0 # Commands created in last 7 days
@property
def average_views_per_command(self) -> float:
"""Calculate average views per command."""
if self.active_commands == 0:
return 0.0
return self.total_views / self.active_commands