import io import pandas as pd import pytest from datetime import date, time from scenar.core import ( read_excel, create_timetable, get_program_types, ScenarsError, parse_inline_schedule, parse_inline_types, ValidationError ) def make_excel_bytes(df: pd.DataFrame) -> bytes: bio = io.BytesIO() with pd.ExcelWriter(bio, engine='openpyxl') as writer: df.to_excel(writer, index=False) return bio.getvalue() def test_read_excel_happy_path(): df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date()], 'Zacatek': ['09:00'], 'Konec': ['10:00'], 'Program': ['Test program'], 'Typ': ['WORKSHOP'], 'Garant': ['Garant Name'], 'Poznamka': ['Pozn'] }) content = make_excel_bytes(df) valid, errors = read_excel(content) assert isinstance(valid, pd.DataFrame) assert len(errors) == 0 assert len(valid) == 1 assert valid.iloc[0]['Program'] == 'Test program' def test_read_excel_invalid_time(): df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date()], 'Zacatek': ['not-a-time'], 'Konec': ['10:00'], 'Program': ['Bad Time'], 'Typ': ['LECTURE'], 'Garant': [None], 'Poznamka': [None] }) content = make_excel_bytes(df) valid, errors = read_excel(content) assert isinstance(errors, list) assert len(errors) >= 1 def test_get_program_types(): """Test form field parsing for program type/color/description.""" form_data = { 'type_code_0': 'WORKSHOP', 'type_code_1': 'LECTURE', 'desc_0': 'Workshop description', 'desc_1': 'Lecture description', 'color_0': '#FF0000', 'color_1': '#00FF00', } # get_program_types returns (descriptions_dict, colors_dict) tuple descriptions, colors = get_program_types(form_data) assert len(descriptions) == 2 assert descriptions['WORKSHOP'] == 'Workshop description' assert descriptions['LECTURE'] == 'Lecture description' assert colors['WORKSHOP'] == 'FFFF0000' # FF prefix added assert colors['LECTURE'] == 'FF00FF00' def test_create_timetable(): """Test Excel timetable generation with properly parsed times.""" from datetime import time # Create test data with time objects (as returned by read_excel) df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date()], 'Zacatek': [time(9, 0)], # Use time object, not string 'Konec': [time(10, 0)], 'Program': ['Test Program'], 'Typ': ['WORKSHOP'], 'Garant': ['John Doe'], 'Poznamka': ['Test note'], }) program_descriptions = { 'WORKSHOP': 'Workshop Type', } program_colors = { 'WORKSHOP': 'FF0070C0', # AARRGGBB format } # create_timetable returns openpyxl.Workbook wb = create_timetable( df, title="Test Timetable", detail="Test Detail", program_descriptions=program_descriptions, program_colors=program_colors ) assert wb is not None assert len(wb.sheetnames) > 0 ws = wb.active assert ws['A1'].value == "Test Timetable" assert ws['A2'].value == "Test Detail" def test_create_timetable_with_color_dict(): """Test timetable generation with separate color dict.""" from datetime import time df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date()], 'Zacatek': [time(9, 0)], # Use time object 'Konec': [time(10, 0)], 'Program': ['Lecture 101'], 'Typ': ['LECTURE'], 'Garant': ['Dr. Smith'], 'Poznamka': [None], }) program_descriptions = { 'LECTURE': 'Standard Lecture', } program_colors = { 'LECTURE': 'FFFF6600', } wb = create_timetable( df, title="Advanced Timetable", detail="With color dict", program_descriptions=program_descriptions, program_colors=program_colors ) assert wb is not None ws = wb.active assert ws['A1'].value == "Advanced Timetable" def test_parse_inline_schedule(): """Test parsing inline schedule form data.""" form_data = { 'datum_0': '2025-11-13', 'zacatek_0': '09:00', 'konec_0': '10:00', 'program_0': 'Test Program', 'typ_0': 'WORKSHOP', 'garant_0': 'John Doe', 'poznamka_0': 'Test note', 'datum_1': '2025-11-13', 'zacatek_1': '10:30', 'konec_1': '11:30', 'program_1': 'Another Program', 'typ_1': 'LECTURE', 'garant_1': 'Jane Smith', 'poznamka_1': '', } df = parse_inline_schedule(form_data) assert isinstance(df, pd.DataFrame) assert len(df) == 2 assert df.iloc[0]['Program'] == 'Test Program' assert df.iloc[0]['Typ'] == 'WORKSHOP' assert df.iloc[1]['Program'] == 'Another Program' assert df.iloc[1]['Typ'] == 'LECTURE' def test_parse_inline_schedule_missing_required(): """Test that missing required fields raise ValidationError.""" form_data = { 'datum_0': '2025-11-13', 'zacatek_0': '09:00', # Missing konec_0, program_0, typ_0 } with pytest.raises(ValidationError): parse_inline_schedule(form_data) def test_parse_inline_types(): """Test parsing inline type definitions.""" form_data = { 'type_name_0': 'WORKSHOP', 'type_desc_0': 'Workshop Type', 'type_color_0': '#0070C0', 'type_name_1': 'LECTURE', 'type_desc_1': 'Lecture Type', 'type_color_1': '#FF6600', } descriptions, colors = parse_inline_types(form_data) assert len(descriptions) == 2 assert descriptions['WORKSHOP'] == 'Workshop Type' assert descriptions['LECTURE'] == 'Lecture Type' assert colors['WORKSHOP'] == 'FF0070C0' assert colors['LECTURE'] == 'FFFF6600' def test_inline_workflow_integration(): """Test end-to-end inline workflow: parse form → create timetable.""" # Schedule form data schedule_form = { 'datum_0': '2025-11-13', 'zacatek_0': '09:00', 'konec_0': '10:00', 'program_0': 'Opening', 'typ_0': 'KEYNOTE', 'garant_0': 'Dr. Smith', 'poznamka_0': 'Start of event', } # Type form data types_form = { 'type_name_0': 'KEYNOTE', 'type_desc_0': 'Keynote Speech', 'type_color_0': '#FF0000', } # Merge forms form_data = {**schedule_form, **types_form} # Parse df = parse_inline_schedule(form_data) descriptions, colors = parse_inline_types(form_data) # Generate timetable wb = create_timetable( df, title="Integration Test Event", detail="Testing inline workflow", program_descriptions=descriptions, program_colors=colors ) assert wb is not None ws = wb.active assert ws['A1'].value == "Integration Test Event" assert ws['A2'].value == "Testing inline workflow" def test_excel_import_to_step2_workflow(): """Test workflow: Excel import -> step 2 (type definition) -> step 3 (generate). This simulates the user uploading Excel, then filling type definitions. """ import base64 # Step 1: Create Excel file df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date()], 'Zacatek': ['09:00'], 'Konec': ['10:00'], 'Program': ['Opening Keynote'], 'Typ': ['KEYNOTE'], 'Garant': ['John Smith'], 'Poznamka': ['Welcome speech'] }) file_content = make_excel_bytes(df) # Step 2: Read Excel (simulating step=2 processing) valid_data, errors = read_excel(file_content) assert len(errors) == 0 assert len(valid_data) == 1 # Extract types program_types = sorted([str(t).strip() for t in valid_data["Typ"].dropna().unique()]) assert program_types == ['KEYNOTE'] # Step 3: User fills type definitions (simulating form submission in step=2) # This would be base64 encoded in the hidden field file_content_base64 = base64.b64encode(file_content).decode('utf-8') form_data = { 'title': 'Test Event', 'detail': 'Test Detail', 'file_content_base64': file_content_base64, 'type_code_0': 'KEYNOTE', 'desc_0': 'Main keynote presentation', 'color_0': '#FF0000', 'step': '3' } # Parse types (simulating step=3 processing) descriptions, colors = get_program_types(form_data) assert descriptions['KEYNOTE'] == 'Main keynote presentation' assert colors['KEYNOTE'] == 'FFFF0000' # Step 4: Generate timetable # Re-read Excel from base64 file_content_decoded = base64.b64decode(form_data['file_content_base64']) data, _ = read_excel(file_content_decoded) wb = create_timetable( data, title=form_data['title'], detail=form_data['detail'], program_descriptions=descriptions, program_colors=colors ) assert wb is not None ws = wb.active assert ws['A1'].value == 'Test Event' assert ws['A2'].value == 'Test Detail' def test_excel_import_to_inline_editor_workflow(): """Test workflow: Excel import -> step 2 -> step 2b (inline editor). This simulates clicking 'Upravit v inline editoru' button. """ import base64 # Create Excel file df = pd.DataFrame({ 'Datum': [pd.Timestamp('2025-11-13').date(), pd.Timestamp('2025-11-14').date()], 'Zacatek': ['09:00', '14:00'], 'Konec': ['10:00', '15:30'], 'Program': ['Morning Session', 'Afternoon Workshop'], 'Typ': ['KEYNOTE', 'WORKSHOP'], 'Garant': ['Alice', 'Bob'], 'Poznamka': ['', 'Hands-on'] }) file_content = make_excel_bytes(df) # Step 2: Read Excel valid_data, errors = read_excel(file_content) assert len(errors) == 0 assert len(valid_data) == 2 # Step 2b: User clicks "Upravit v inline editoru" # The form would pass file_content_base64 to step=2b file_content_base64 = base64.b64encode(file_content).decode('utf-8') # Simulate step 2b: decode and re-read Excel file_content_decoded = base64.b64decode(file_content_base64) data_in_editor, _ = read_excel(file_content_decoded) # Verify data loaded correctly assert len(data_in_editor) == 2 assert data_in_editor.iloc[0]['Program'] == 'Morning Session' assert data_in_editor.iloc[1]['Program'] == 'Afternoon Workshop' # Extract types for editor program_types = sorted([str(t).strip() for t in data_in_editor["Typ"].dropna().unique()]) assert set(program_types) == {'KEYNOTE', 'WORKSHOP'}