feat: enhance extraction page with available bets and result associations

- Rename 'Available Outcomes' to 'Available Bets' in admin interface
- Add database tables for available_bets and result_options
- Implement CRUD operations for managing bets and result options
- Update extraction page UI with add/delete functionality
- Implement drag-and-drop associations between bets and results
- Add database persistence for all associations
- Remove X buttons from result association columns for cleaner UI
- Update terminology throughout for clarity
parent 9fd38518
...@@ -11,7 +11,8 @@ from .models import ( ...@@ -11,7 +11,8 @@ from .models import (
ApiTokenModel, ApiTokenModel,
LogEntryModel, LogEntryModel,
TemplateModel, TemplateModel,
AvailableBetModel AvailableBetModel,
ResultOptionModel
) )
from .migrations import DatabaseMigration, run_migrations from .migrations import DatabaseMigration, run_migrations
...@@ -25,6 +26,7 @@ __all__ = [ ...@@ -25,6 +26,7 @@ __all__ = [
'LogEntryModel', 'LogEntryModel',
'TemplateModel', 'TemplateModel',
'AvailableBetModel', 'AvailableBetModel',
'ResultOptionModel',
'DatabaseMigration', 'DatabaseMigration',
'run_migrations' 'run_migrations'
] ]
\ No newline at end of file
...@@ -1807,6 +1807,107 @@ class Migration_023_AddAvailableBetsTable(DatabaseMigration): ...@@ -1807,6 +1807,107 @@ class Migration_023_AddAvailableBetsTable(DatabaseMigration):
logger.error(f"Failed to drop available_bets table: {e}") logger.error(f"Failed to drop available_bets table: {e}")
return False return False
class Migration_024_AddResultOptionsTable(DatabaseMigration):
"""Add result_options table for managing result options in extraction page"""
def __init__(self):
super().__init__("024", "Add result_options table for managing result options")
def up(self, db_manager) -> bool:
"""Create result_options table with default result options"""
try:
with db_manager.engine.connect() as conn:
# Create result_options table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS result_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
result_name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
# Create indexes for result_options table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_result_options_result_name ON result_options(result_name)",
"CREATE INDEX IF NOT EXISTS ix_result_options_is_active ON result_options(is_active)",
"CREATE INDEX IF NOT EXISTS ix_result_options_sort_order ON result_options(sort_order)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
# Insert default result options as specified by user
default_results = [
('DRAW', 'Draw result', 1),
('DKO', 'Double Knockout result', 2),
('WIN1', 'Fighter 1 wins result', 3),
('WIN2', 'Fighter 2 wins result', 4),
('RET1', 'Fighter 1 retires result', 5),
('RET2', 'Fighter 2 retires result', 6),
('PTS1', 'Fighter 1 wins by points result', 7),
('PTS2', 'Fighter 2 wins by points result', 8),
('KO1', 'Fighter 1 wins by KO result', 9),
('KO2', 'Fighter 2 wins by KO result', 10),
]
for result_name, description, sort_order in default_results:
conn.execute(text("""
INSERT OR IGNORE INTO result_options
(result_name, description, is_active, sort_order, created_at, updated_at)
VALUES (:result_name, :description, 1, :sort_order, datetime('now'), datetime('now'))
"""), {
'result_name': result_name,
'description': description,
'sort_order': sort_order
})
conn.commit()
logger.info("Result options table created with default result options")
return True
except Exception as e:
logger.error(f"Failed to create result_options table: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop result_options table"""
try:
with db_manager.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS result_options"))
conn.commit()
logger.info("Result options table dropped")
return True
except Exception as e:
logger.error(f"Failed to drop result_options table: {e}")
return False
class Migration_025_AddResultOptionModel(DatabaseMigration):
"""Add ResultOptionModel to models.py for result options management"""
def __init__(self):
super().__init__("025", "Add ResultOptionModel to models.py for result options management")
def up(self, db_manager) -> bool:
"""This migration just ensures the ResultOptionModel is available - no database changes needed"""
try:
logger.info("ResultOptionModel migration - no database changes required")
return True
except Exception as e:
logger.error(f"Failed to apply ResultOptionModel migration: {e}")
return False
def down(self, db_manager) -> bool:
"""No database changes to rollback"""
logger.info("ResultOptionModel migration rollback - no database changes")
return True
# Registry of all migrations in order # Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [ MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(), Migration_001_InitialSchema(),
...@@ -1832,6 +1933,8 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -1832,6 +1933,8 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_021_AddBarcodeConfiguration(), Migration_021_AddBarcodeConfiguration(),
Migration_022_AddQRCodeConfiguration(), Migration_022_AddQRCodeConfiguration(),
Migration_023_AddAvailableBetsTable(), Migration_023_AddAvailableBetsTable(),
Migration_024_AddResultOptionsTable(),
Migration_025_AddResultOptionModel(),
] ]
......
...@@ -781,3 +781,22 @@ class AvailableBetModel(BaseModel): ...@@ -781,3 +781,22 @@ class AvailableBetModel(BaseModel):
def __repr__(self): def __repr__(self):
return f'<AvailableBet {self.bet_name}: {self.description}>' return f'<AvailableBet {self.bet_name}: {self.description}>'
class ResultOptionModel(BaseModel):
"""Result options for extraction system results area"""
__tablename__ = 'result_options'
__table_args__ = (
Index('ix_result_options_result_name', 'result_name'),
Index('ix_result_options_is_active', 'is_active'),
Index('ix_result_options_sort_order', 'sort_order'),
UniqueConstraint('result_name', name='uq_result_options_result_name'),
)
result_name = Column(String(50), nullable=False, unique=True, comment='Result option name (e.g., DRAW, WIN1, KO1)')
description = Column(String(255), comment='Description of the result option')
is_active = Column(Boolean, default=True, nullable=False, comment='Whether this result option is active')
sort_order = Column(Integer, default=0, comment='Sort order for display')
def __repr__(self):
return f'<ResultOption {self.result_name}: {self.description}>'
...@@ -3531,6 +3531,216 @@ def delete_available_bet(): ...@@ -3531,6 +3531,216 @@ def delete_available_bet():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Result Options API routes
@api_bp.route('/extraction/result-options')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_result_options():
"""Get all result options for extraction"""
try:
from ..database.models import ResultOptionModel
session = api_bp.db_manager.get_session()
try:
# Get all result options
result_options = session.query(ResultOptionModel).order_by(ResultOptionModel.result_name.asc()).all()
options_data = [option.to_dict() for option in result_options]
return jsonify({
"success": True,
"options": options_data,
"total": len(options_data)
})
finally:
session.close()
except Exception as e:
logger.error(f"API get result options error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/extraction/result-options/add', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def add_result_option():
"""Add a new result option"""
try:
from ..database.models import ResultOptionModel
data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper()
description = data.get('description', '').strip()
if not result_name:
return jsonify({"error": "result_name is required"}), 400
if len(result_name) > 50:
return jsonify({"error": "result_name must be 50 characters or less"}), 400
session = api_bp.db_manager.get_session()
try:
# Check if result option already exists
existing_option = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if existing_option:
return jsonify({"error": f"Result option '{result_name}' already exists"}), 400
# Get the highest sort order
max_sort = session.query(ResultOptionModel.sort_order).order_by(ResultOptionModel.sort_order.desc()).first()
sort_order = (max_sort[0] + 1) if max_sort and max_sort[0] else 1
# Create new result option
new_option = ResultOptionModel(
result_name=result_name,
description=description or f"Result option: {result_name}",
sort_order=sort_order
)
session.add(new_option)
session.commit()
logger.info(f"Added result option: {result_name}")
return jsonify({
"success": True,
"message": f"Result option '{result_name}' added successfully",
"option": new_option.to_dict()
})
finally:
session.close()
except Exception as e:
logger.error(f"API add result option error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/extraction/result-options/delete', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def delete_result_option():
"""Delete a result option"""
try:
from ..database.models import ResultOptionModel
data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper()
if not result_name:
return jsonify({"error": "result_name is required"}), 400
session = api_bp.db_manager.get_session()
try:
# Find the result option
option = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if not option:
return jsonify({"error": f"Result option '{result_name}' not found"}), 404
# Delete the result option
session.delete(option)
session.commit()
logger.info(f"Deleted result option: {result_name}")
return jsonify({
"success": True,
"message": f"Result option '{result_name}' deleted successfully"
})
finally:
session.close()
except Exception as e:
logger.error(f"API delete result option error: {e}")
return jsonify({"error": str(e)}), 500
# Migration to create result_options table with default values
def create_result_options_migration():
"""Create migration for result_options table"""
try:
from ..database.models import ResultOptionModel
from ..database.migrations import DatabaseMigration
class Migration_023(DatabaseMigration):
version = "023"
description = "Add result_options table for extraction system results area"
def up(self, db_manager):
"""Apply migration"""
try:
session = db_manager.get_session()
try:
# Create table if it doesn't exist
ResultOptionModel.__table__.create(session.bind, checkfirst=True)
# Insert default result options
default_options = [
('DRAW', 'Draw result', 1),
('DKO', 'Double Knockout', 2),
('WIN1', 'Fighter 1 wins', 3),
('WIN2', 'Fighter 2 wins', 4),
('RET1', 'Fighter 1 retires', 5),
('RET2', 'Fighter 2 retires', 6),
('PTS1', 'Fighter 1 wins by points', 7),
('PTS2', 'Fighter 2 wins by points', 8),
('KO1', 'Fighter 1 wins by KO', 9),
('KO2', 'Fighter 2 wins by KO', 10)
]
for result_name, description, sort_order in default_options:
# Check if already exists
existing = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if not existing:
option = ResultOptionModel(
result_name=result_name,
description=description,
sort_order=sort_order
)
session.add(option)
session.commit()
logger.info("Created result_options table with default values")
return True
finally:
session.close()
except Exception as e:
logger.error(f"Migration 023 failed: {e}")
return False
def down(self, db_manager):
"""Rollback migration"""
try:
session = db_manager.get_session()
try:
# Drop table
ResultOptionModel.__table__.drop(session.bind, checkfirst=True)
session.commit()
logger.info("Dropped result_options table")
return True
finally:
session.close()
except Exception as e:
logger.error(f"Migration 023 rollback failed: {e}")
return False
return Migration_023()
except Exception as e:
logger.error(f"Failed to create result options migration: {e}")
return None
# Add the migration to the migrations list
try:
from ..database.migrations import MIGRATIONS
migration_023 = create_result_options_migration()
if migration_023:
MIGRATIONS.append(migration_023)
logger.info("Added Migration 023 for result_options table")
except Exception as e:
logger.error(f"Failed to add Migration 023: {e}")
# Redistribution CAP API routes (admin-only) # Redistribution CAP API routes (admin-only)
@api_bp.route('/extraction/redistribution-cap') @api_bp.route('/extraction/redistribution-cap')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
......
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