Files
scenar-creator/tests/test_read_excel.py
Martin Sukany 2f4c930739
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Fix: step 2 -> step 2b/3 workflow
Problem:
- Když uživatel importoval Excel a klikl 'Upravit v inline editoru' nebo 'Generovat',
  dostal chybu 'Neplatný krok nebo chybějící data'
- step 2b vyžadoval file_item, ale formulář posílal file_content_base64

Řešení:
- Upravena podmínka pro step=2b aby akceptovala i file_content_base64
- Přidány 2 nové testy:
  * test_excel_import_to_step2_workflow - testuje import -> step 2 -> step 3 (generate)
  * test_excel_import_to_inline_editor_workflow - testuje import -> step 2 -> step 2b (inline editor)

Testy: 18/18 unit testů prošlo 
2025-11-13 16:15:23 +01:00

362 lines
11 KiB
Python

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'}