Changing the fixgture today logic

parent 4b0a8e87
......@@ -2,7 +2,7 @@
All notable changes to this project will be documented in this file.
## [1.0.11] - 2026-01-20
## [1.0.12] - 2026-01-26
### Added
- **Venue Timezone System**: Complete timezone-aware system to fix UTC midnight day change issues
......@@ -30,6 +30,22 @@ All notable changes to this project will be documented in this file.
- **Backward Compatibility**: Existing UTC data remains intact while new operations use venue timezone
- **Fallback Strategy**: System timezone → Environment variable (VENUE_TIMEZONE) → UTC ultimate fallback
### Added
- **Match Templates Database Tables**: New `matches_templates` and `match_outcomes_templates` tables for storing reusable match configurations
- **Template Model Classes**: `MatchTemplateModel` and `MatchOutcomeTemplateModel` SQLAlchemy models with full relationship support
- **Database Migration System**: Migration_036_AddMatchTemplatesTables with comprehensive indexing and foreign key relationships
- **Template Storage Architecture**: Complete database schema for storing match templates with outcomes and metadata
- **Fixture Synchronization Refactor**: Modified fixture update process to use template tables instead of regular match tables
- **Template Cleanup Logic**: Automatic cleanup of old fixture templates when new fixtures are fully validated
- **Template-Based Fixture Management**: New workflow where fixtures are stored in templates first, then cleaned up after validation
### Technical Details
- **Database Schema**: Added matches_templates and match_outcomes_templates tables with identical structure to matches/match_outcomes
- **Foreign Key Relationships**: Proper foreign key constraints between template tables with CASCADE delete support
- **Indexing Strategy**: Comprehensive indexing for optimal query performance on template data
- **Model Integration**: New template models fully integrated into database module exports
- **Migration System**: Automated table creation with proper SQLite syntax and constraint handling
## [1.2.11] - 2025-08-28
### Fixed
......
......@@ -14,7 +14,7 @@ from typing import List, Dict, Any
# Build configuration
BUILD_CONFIG = {
'app_name': 'MbetterClient',
'app_version': '1.0.11',
'app_version': '1.0.12',
'description': 'Cross-platform multimedia client application',
'author': 'MBetter Team',
'entry_point': 'main.py',
......
......@@ -211,7 +211,7 @@ Examples:
parser.add_argument(
'--version',
action='version',
version='MbetterClient 1.0.11'
version='MbetterClient 1.0.12'
)
# Timer options
......
......@@ -4,7 +4,7 @@ MbetterClient - Cross-platform multimedia client application
A multi-threaded application with video playback, web dashboard, and REST API integration.
"""
__version__ = "1.0.11"
__version__ = "1.0.12"
__author__ = "MBetter Project"
__email__ = "dev@mbetter.net"
__description__ = "Cross-platform multimedia client with video overlay and web dashboard"
......
......@@ -21,7 +21,7 @@ from ..core.message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..config.settings import ApiConfig
from ..config.manager import ConfigManager
from ..database.manager import DatabaseManager
from ..database.models import MatchModel, MatchOutcomeModel
from ..database.models import MatchModel, MatchOutcomeModel, MatchTemplateModel, MatchOutcomeTemplateModel
from ..config.settings import get_user_data_dir
from ..utils.ssl_utils import create_requests_session_with_ssl_support
......@@ -365,32 +365,32 @@ class UpdatesResponseHandler(ResponseHandler):
return self.handle_error(endpoint, e)
def _synchronize_match(self, session, match_data: Dict[str, Any]):
"""Synchronize match data to database"""
"""Synchronize match data to template database tables"""
try:
match_number = match_data.get('match_number')
fixture_id = match_data.get('fixture_id')
if not match_number or not fixture_id:
logger.warning(f"Skipping match with missing match_number ({match_number}) or fixture_id ({fixture_id})")
return
# Check if match already exists by both match_number AND fixture_id
existing_match = session.query(MatchModel).filter_by(
# Check if match already exists by both match_number AND fixture_id in template tables
existing_match = session.query(MatchTemplateModel).filter_by(
match_number=match_number,
fixture_id=fixture_id
).first()
if existing_match:
# Update existing match
# Update existing match template
for key, value in match_data.items():
if hasattr(existing_match, key) and key not in ['id', 'created_at']:
setattr(existing_match, key, value)
existing_match.updated_at = datetime.utcnow()
match = existing_match
logger.debug(f"Updated existing match {match_number} for fixture {fixture_id}")
logger.debug(f"Updated existing match template {match_number} for fixture {fixture_id}")
else:
# Create new match
match = MatchModel(
# Create new match template
match = MatchTemplateModel(
match_number=match_data.get('match_number'),
fighter1_township=match_data.get('fighter1_township', ''),
fighter2_township=match_data.get('fighter2_township', ''),
......@@ -408,31 +408,32 @@ class UpdatesResponseHandler(ResponseHandler):
zip_upload_progress=match_data.get('zip_upload_progress', 0.0),
done=match_data.get('done', False),
running=match_data.get('running', False),
fixture_active_time=match_data.get('fixture_active_time')
fixture_active_time=match_data.get('fixture_active_time'),
created_by=1 # Default admin user
)
session.add(match)
logger.debug(f"Created new match {match_number} for fixture {fixture_id}")
logger.debug(f"Created new match template {match_number} for fixture {fixture_id}")
# Flush to get the match ID for outcomes
session.flush()
# Handle match outcomes
# Handle match outcomes in template table
outcomes_data = match_data.get('outcomes', {})
if outcomes_data:
# Remove existing outcomes
session.query(MatchOutcomeModel).filter_by(match_id=match.id).delete()
session.query(MatchOutcomeTemplateModel).filter_by(match_id=match.id).delete()
# Add new outcomes
for column_name, float_value in outcomes_data.items():
outcome = MatchOutcomeModel(
outcome = MatchOutcomeTemplateModel(
match_id=match.id,
column_name=column_name,
float_value=float(float_value)
)
session.add(outcome)
except Exception as e:
logger.error(f"Failed to synchronize match: {e}")
logger.error(f"Failed to synchronize match template: {e}")
# Rollback this session to clean state
session.rollback()
raise
......@@ -613,6 +614,53 @@ class UpdatesResponseHandler(ResponseHandler):
logger.error(f"Failed to validate ZIP file {zip_path}: {e}")
return False
def _cleanup_old_fixture_templates(self, session, fixtures):
"""Clean up old fixture templates and keep only the new ones"""
try:
# Get fixture IDs from the current fixtures that were just synchronized
current_fixture_ids = set()
for fixture_data in fixtures:
fixture_id = fixture_data.get('fixture_id')
if fixture_id:
current_fixture_ids.add(fixture_id)
if not current_fixture_ids:
logger.warning("No current fixture IDs found - skipping cleanup")
return
logger.info(f"Cleaning up old fixture templates - keeping fixtures: {current_fixture_ids}")
# Get all fixture IDs currently in the template tables
existing_fixture_ids = set()
existing_fixtures = session.query(MatchTemplateModel.fixture_id).distinct().all()
for row in existing_fixtures:
existing_fixture_ids.add(row.fixture_id)
# Find fixture IDs to remove (existing ones not in current fixtures)
fixtures_to_remove = existing_fixture_ids - current_fixture_ids
if not fixtures_to_remove:
logger.info("No old fixture templates to remove")
return
logger.info(f"Removing old fixture templates: {fixtures_to_remove}")
# Delete matches from old fixtures (CASCADE will delete outcomes)
deleted_matches = 0
for fixture_id in fixtures_to_remove:
deleted_count = session.query(MatchTemplateModel).filter(
MatchTemplateModel.fixture_id == fixture_id
).delete()
deleted_matches += deleted_count
logger.info(f"Removed fixture template {fixture_id} with {deleted_count} matches")
session.commit()
logger.info(f"Successfully cleaned up {len(fixtures_to_remove)} old fixture templates, removed {deleted_matches} matches total")
except Exception as e:
logger.error(f"Failed to cleanup old fixture templates: {e}")
session.rollback()
def _send_download_progress(self, downloaded: int, total: int, message: str):
"""Send download progress update via message bus"""
try:
......@@ -1210,6 +1258,10 @@ class APIClient(ThreadedComponent):
if downloaded_zips == expected_zips:
logger.info(f"Fixture update completed successfully - {synchronized_matches} matches synchronized, {downloaded_zips}/{expected_zips} ZIPs downloaded")
logger.debug("All expected ZIP files downloaded - fixtures are ready for games")
# Clean up old fixture templates and keep only the new ones
self._cleanup_old_fixture_templates(session, fixtures)
# Send a message to trigger game start check
game_start_check_message = Message(
type=MessageType.SYSTEM_STATUS,
......
......@@ -262,7 +262,7 @@ class ApiConfig:
# Request settings
verify_ssl: bool = True
user_agent: str = "MbetterClient/1.0r11"
user_agent: str = "MbetterClient/1.0r12"
max_response_size_mb: int = 100
# Additional API client settings
......@@ -366,7 +366,7 @@ class AppSettings:
timer: TimerConfig = field(default_factory=TimerConfig)
# Application settings
version: str = "1.0.11"
version: str = "1.0.12"
debug_mode: bool = False
dev_message: bool = False # Enable debug mode showing only message bus messages
debug_messages: bool = False # Show all messages passing through the message bus on screen
......
This diff is collapsed.
......@@ -262,9 +262,22 @@ class MatchTimerComponent(ThreadedComponent):
self._send_timer_update()
logger.info(f"Timer updated with fixture {match_info['fixture_id']}")
else:
logger.info("No more matches to start, stopping timer")
logger.info("No more matches available and none could be created - stopping timer and exiting game")
self._stop_timer()
# Send message to exit game status since no matches are available
exit_game_message = Message(
type=MessageType.GAME_STATUS,
sender=self.name,
data={
"status": "completed_no_matches_available",
"fixture_id": fixture_id,
"timestamp": time.time()
}
)
self.message_bus.publish(exit_game_message, broadcast=True)
logger.info(f"Sent game exit message due to no matches available in fixture {fixture_id}")
except Exception as e:
logger.error(f"Failed to handle NEXT_MATCH message: {e}")
# On error, try to restart timer anyway
......@@ -508,25 +521,31 @@ class MatchTimerComponent(ThreadedComponent):
return 0
def _ensure_minimum_matches_in_fixture(self, fixture_id: str, minimum_required: int, session):
"""Ensure fixture has at least minimum_required matches by creating new ones from old completed matches"""
"""Ensure fixture has at least minimum_required matches by creating new ones from templates or old matches"""
try:
from ..database.models import MatchModel
logger.info(f"Ensuring fixture {fixture_id} has at least {minimum_required} additional matches")
# Get the last played match ID to exclude it from selection
last_played_match_id = self._get_last_played_match_id(fixture_id, session)
logger.info(f"Last played match ID: {last_played_match_id}")
# First try: Select random match templates
template_matches = self._select_random_match_templates(minimum_required, session)
if template_matches:
logger.info(f"Selected {len(template_matches)} match templates to create new matches")
self._create_matches_from_templates(fixture_id, template_matches, session)
logger.info(f"Created {len(template_matches)} new matches in fixture {fixture_id} from templates")
return
# Select random completed matches, excluding the last played one
old_matches = self._select_random_completed_matches_excluding_last(minimum_required, last_played_match_id, session)
# Second try: Select random completed matches from matches table
logger.info("No match templates available, trying to reuse old completed matches")
old_matches = self._select_random_completed_matches_excluding_last(minimum_required, None, session)
if old_matches:
logger.info(f"Selected {len(old_matches)} old matches to create new ones")
logger.info(f"Selected {len(old_matches)} old completed matches to create new matches")
self._create_matches_from_old_matches(fixture_id, old_matches, session)
logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id}")
else:
logger.warning(f"No suitable old matches found to create new ones for fixture {fixture_id}")
logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id} from old matches")
return
# No matches available at all
logger.warning(f"No match templates or old completed matches found - cannot create new matches for fixture {fixture_id}")
except Exception as e:
logger.error(f"Failed to ensure minimum matches in fixture {fixture_id}: {e}")
......@@ -725,6 +744,94 @@ class MatchTimerComponent(ThreadedComponent):
# Default fallback
return 'all_bets_on_start'
def _select_random_match_templates(self, count: int, session) -> List[Any]:
"""Select random match templates from the database"""
try:
from ..database.models import MatchTemplateModel
from sqlalchemy.orm import joinedload
# Get all active match templates
match_templates = session.query(MatchTemplateModel).options(joinedload(MatchTemplateModel.outcomes)).filter(
MatchTemplateModel.active_status == True
).all()
if len(match_templates) < count:
logger.warning(f"Only {len(match_templates)} match templates found, requested {count}")
return match_templates
# Select random templates
selected_templates = random.sample(match_templates, count)
logger.info(f"Selected {len(selected_templates)} random match templates")
return selected_templates
except Exception as e:
logger.error(f"Failed to select random match templates: {e}")
return []
def _create_matches_from_templates(self, fixture_id: str, template_matches: List[Any], session):
"""Create new matches in the fixture by copying from match templates"""
try:
from ..database.models import MatchModel, MatchOutcomeModel
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# Find the maximum match_number in the fixture and increment from there
max_match_number = session.query(MatchModel.match_number).filter(
MatchModel.fixture_id == fixture_id
).order_by(MatchModel.match_number.desc()).first()
match_number = (max_match_number[0] + 1) if max_match_number else 1
for template_match in template_matches:
# Create a new match based on the template
new_match = MatchModel(
match_number=match_number,
fighter1_township=template_match.fighter1_township,
fighter2_township=template_match.fighter2_township,
venue_kampala_township=template_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=template_match.filename,
file_sha1sum=template_match.file_sha1sum,
active_status=True,
zip_filename=template_match.zip_filename,
zip_sha1sum=template_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
zip_validation_status='valid', # ZIP already validated from template
fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes from template
for template_outcome in template_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created new match #{match_number} from template #{template_match.match_number} with status {new_match_status}")
match_number += 1
session.commit()
logger.info(f"Created {len(template_matches)} new matches in fixture {fixture_id} with status {new_match_status}")
except Exception as e:
logger.error(f"Failed to create matches from templates: {e}")
session.rollback()
raise
def _send_timer_update(self):
"""Send timer update message to all clients"""
try:
......
......@@ -12,7 +12,9 @@ from .models import (
LogEntryModel,
TemplateModel,
AvailableBetModel,
ResultOptionModel
ResultOptionModel,
MatchTemplateModel,
MatchOutcomeTemplateModel
)
from .migrations import DatabaseMigration, run_migrations
......@@ -27,6 +29,8 @@ __all__ = [
'TemplateModel',
'AvailableBetModel',
'ResultOptionModel',
'MatchTemplateModel',
'MatchOutcomeTemplateModel',
'DatabaseMigration',
'run_migrations'
]
\ No newline at end of file
......@@ -181,7 +181,6 @@ class DatabaseManager:
if not self._initialized:
raise RuntimeError("Database manager not initialized")
session = self.Session()
logger.debug(f"DEBUG: Database manager returning session for database: {self.db_path}")
return session
def close(self):
......
......@@ -2731,6 +2731,121 @@ class Migration_035_AddDailyRedistributionShortfallTable(DatabaseMigration):
return False
class Migration_036_AddMatchTemplatesTables(DatabaseMigration):
"""Add matches_templates and match_outcomes_templates tables for storing match templates"""
def __init__(self):
super().__init__("036", "Add matches_templates and match_outcomes_templates tables for storing match templates")
def up(self, db_manager) -> bool:
"""Create matches_templates and match_outcomes_templates tables"""
try:
with db_manager.engine.connect() as conn:
# Create matches_templates table (identical structure to matches table)
conn.execute(text("""
CREATE TABLE IF NOT EXISTS matches_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_number INTEGER NOT NULL,
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
winning_outcomes TEXT NULL,
under_over_result VARCHAR(50) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
zip_validation_status VARCHAR(20) DEFAULT 'pending',
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(fixture_id, match_number)
)
"""))
# Create match_outcomes_templates table (identical structure to match_outcomes table)
conn.execute(text("""
CREATE TABLE IF NOT EXISTS match_outcomes_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_id INTEGER NOT NULL REFERENCES matches_templates(id) ON DELETE CASCADE,
column_name VARCHAR(255) NOT NULL,
float_value REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(match_id, column_name)
)
"""))
# Create indexes for matches_templates table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_matches_templates_match_number ON matches_templates(match_number)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_fixture_id ON matches_templates(fixture_id)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_active_status ON matches_templates(active_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_file_sha1sum ON matches_templates(file_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_zip_sha1sum ON matches_templates(zip_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_zip_upload_status ON matches_templates(zip_upload_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_created_by ON matches_templates(created_by)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_fixture_active_time ON matches_templates(fixture_active_time)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_done ON matches_templates(done)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_running ON matches_templates(running)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_status ON matches_templates(status)",
"CREATE INDEX IF NOT EXISTS ix_matches_templates_composite ON matches_templates(active_status, zip_upload_status, created_at)",
]
# Create indexes for match_outcomes_templates table
indexes.extend([
"CREATE INDEX IF NOT EXISTS ix_match_outcomes_templates_match_id ON match_outcomes_templates(match_id)",
"CREATE INDEX IF NOT EXISTS ix_match_outcomes_templates_column_name ON match_outcomes_templates(column_name)",
"CREATE INDEX IF NOT EXISTS ix_match_outcomes_templates_float_value ON match_outcomes_templates(float_value)",
"CREATE INDEX IF NOT EXISTS ix_match_outcomes_templates_composite ON match_outcomes_templates(match_id, column_name)",
])
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Matches templates and match outcomes templates tables created successfully")
return True
except Exception as e:
logger.error(f"Failed to create match templates tables: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop matches_templates and match_outcomes_templates tables"""
try:
with db_manager.engine.connect() as conn:
# Drop tables in reverse order (child first due to foreign keys)
conn.execute(text("DROP TABLE IF EXISTS match_outcomes_templates"))
conn.execute(text("DROP TABLE IF EXISTS matches_templates"))
conn.commit()
logger.info("Matches templates and match outcomes templates tables dropped")
return True
except Exception as e:
logger.error(f"Failed to drop match templates tables: {e}")
return False
# Registry of all migrations in order
# Registry of all migrations in order
......@@ -2770,6 +2885,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_033_AddBarcodeFieldsToBets(),
Migration_034_AddDefaultLicenseText(),
Migration_035_AddDailyRedistributionShortfallTable(),
Migration_036_AddMatchTemplatesTables(),
]
......
......@@ -964,3 +964,148 @@ class DailyRedistributionShortfallModel(BaseModel):
def __repr__(self):
return f'<DailyRedistributionShortfall {self.date}: shortfall={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>'
class MatchTemplateModel(BaseModel):
"""Match templates for storing reusable match configurations"""
__tablename__ = 'matches_templates'
__table_args__ = (
Index('ix_matches_templates_match_number', 'match_number'),
Index('ix_matches_templates_fixture_id', 'fixture_id'),
Index('ix_matches_templates_active_status', 'active_status'),
Index('ix_matches_templates_file_sha1sum', 'file_sha1sum'),
Index('ix_matches_templates_zip_sha1sum', 'zip_sha1sum'),
Index('ix_matches_templates_zip_upload_status', 'zip_upload_status'),
Index('ix_matches_templates_zip_validation_status', 'zip_validation_status'),
Index('ix_matches_templates_created_by', 'created_by'),
Index('ix_matches_templates_fixture_active_time', 'fixture_active_time'),
Index('ix_matches_templates_done', 'done'),
Index('ix_matches_templates_running', 'running'),
Index('ix_matches_templates_status', 'status'),
Index('ix_matches_templates_composite', 'active_status', 'zip_upload_status', 'created_at'),
UniqueConstraint('fixture_id', 'match_number', name='uq_matches_templates_fixture_match'),
)
# Core match data from fixture file
match_number = Column(Integer, nullable=False, comment='Match # from fixture file')
fighter1_township = Column(String(255), nullable=False, comment='Fighter1 (Township)')
fighter2_township = Column(String(255), nullable=False, comment='Fighter2 (Township)')
venue_kampala_township = Column(String(255), nullable=False, comment='Venue (Kampala Township)')
# Match timing and results
start_time = Column(DateTime, comment='Match start time')
end_time = Column(DateTime, comment='Match end time')
result = Column(String(255), comment='Match result/outcome (main result only, e.g. RET2)')
winning_outcomes = Column(JSON, comment='Array of winning outcomes from extraction associations (e.g., ["WIN1", "X1", "12"])')
under_over_result = Column(String(50), comment='UNDER/OVER result if applicable')
done = Column(Boolean, default=False, nullable=False, comment='Match completion flag (0=pending, 1=done)')
running = Column(Boolean, default=False, nullable=False, comment='Match running flag (0=not running, 1=running)')
status = Column(Enum('pending', 'scheduled', 'bet', 'ingame', 'done', 'cancelled', 'failed', 'paused'), default='pending', nullable=False, comment='Match status enum')
fixture_active_time = Column(Integer, nullable=True, comment='Unix timestamp when fixture became active on server')
# File metadata
filename = Column(String(1024), nullable=False, comment='Original fixture filename')
file_sha1sum = Column(String(255), nullable=False, comment='SHA1 checksum of fixture file')
fixture_id = Column(String(255), nullable=False, comment='Fixture identifier (multiple matches can share same fixture)')
active_status = Column(Boolean, default=False, nullable=False, comment='Active status flag')
# ZIP file related fields
zip_filename = Column(String(1024), comment='Associated ZIP filename')
zip_sha1sum = Column(String(255), comment='SHA1 checksum of ZIP file')
zip_upload_status = Column(String(20), default='pending', comment='Upload status: pending, uploading, completed, failed')
zip_upload_progress = Column(Float, default=0.0, comment='Upload progress percentage (0.0-100.0)')
zip_validation_status = Column(String(20), default='pending', comment='Validation status: pending, validating, valid, invalid, failed')
# User tracking
created_by = Column(Integer, ForeignKey('users.id'), comment='User who created this record')
# Relationships
creator = relationship('UserModel', foreign_keys=[created_by])
outcomes = relationship('MatchOutcomeTemplateModel', back_populates='match', cascade='all, delete-orphan')
def is_upload_pending(self) -> bool:
"""Check if ZIP upload is pending"""
return self.zip_upload_status == 'pending'
def is_upload_in_progress(self) -> bool:
"""Check if ZIP upload is in progress"""
return self.zip_upload_status == 'uploading'
def is_upload_completed(self) -> bool:
"""Check if ZIP upload is completed"""
return self.zip_upload_status == 'completed'
def is_upload_failed(self) -> bool:
"""Check if ZIP upload failed"""
return self.zip_upload_status == 'failed'
def set_upload_status(self, status: str, progress: float = None):
"""Set upload status and optionally progress"""
valid_statuses = ['pending', 'uploading', 'completed', 'failed']
if status not in valid_statuses:
raise ValueError(f"Invalid status: {status}. Must be one of {valid_statuses}")
self.zip_upload_status = status
if progress is not None:
self.zip_upload_progress = min(100.0, max(0.0, progress))
self.updated_at = datetime.utcnow()
def activate(self):
"""Activate this match"""
self.active_status = True
self.updated_at = datetime.utcnow()
def deactivate(self):
"""Deactivate this match"""
self.active_status = False
self.updated_at = datetime.utcnow()
def get_outcomes_dict(self) -> Dict[str, float]:
"""Get match outcomes as a dictionary"""
return {outcome.column_name: outcome.float_value for outcome in self.outcomes}
def add_outcome(self, column_name: str, float_value: float):
"""Add or update match outcome"""
# Check if outcome already exists
existing = next((o for o in self.outcomes if o.column_name == column_name), None)
if existing:
existing.float_value = float_value
existing.updated_at = datetime.utcnow()
else:
outcome = MatchOutcomeTemplateModel(
column_name=column_name,
float_value=float_value
)
self.outcomes.append(outcome)
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with outcomes"""
result = super().to_dict(exclude_fields)
result['outcomes'] = self.get_outcomes_dict()
result['outcome_count'] = len(self.outcomes)
return result
def __repr__(self):
return f'<MatchTemplate #{self.match_number}: {self.fighter1_township} vs {self.fighter2_township}>'
class MatchOutcomeTemplateModel(BaseModel):
"""Match outcome template values for storing reusable match configurations"""
__tablename__ = 'match_outcomes_templates'
__table_args__ = (
Index('ix_match_outcomes_templates_match_id', 'match_id'),
Index('ix_match_outcomes_templates_column_name', 'column_name'),
Index('ix_match_outcomes_templates_float_value', 'float_value'),
Index('ix_match_outcomes_templates_composite', 'match_id', 'column_name'),
UniqueConstraint('match_id', 'column_name', name='uq_match_outcomes_templates_match_column'),
)
match_id = Column(Integer, ForeignKey('matches_templates.id', ondelete='CASCADE'), nullable=False, comment='Foreign key to matches_templates table')
column_name = Column(String(255), nullable=False, comment='Result column name from fixture file')
float_value = Column(Float, nullable=False, comment='Float value with precision')
# Relationships
match = relationship('MatchTemplateModel', back_populates='outcomes')
def __repr__(self):
return f'<MatchOutcomeTemplate {self.column_name}={self.float_value} for MatchTemplate {self.match_id}>'
......@@ -209,7 +209,7 @@ class WebDashboard(ThreadedComponent):
def inject_globals():
return {
'app_name': 'MbetterClient',
'app_version': '1.0.11',
'app_version': '1.0.12',
'current_time': time.time(),
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment