feat: v3.0 - canvas editor, JSON-only, no Excel, new UI
Some checks failed
Build & Push Docker / build (push) Has been cancelled

- Remove all Excel code (import, export, template, pandas, openpyxl)
- New canvas-based schedule editor with drag & drop (interact.js)
- Modern 3-panel UI: sidebar, canvas, documentation tab
- New data model: Block with id/date/start/end, ProgramType with id/name/color
- Clean API: GET /api/health, POST /api/validate, GET /api/sample, POST /api/generate-pdf
- Rewritten PDF generator using ScenarioDocument directly (no DataFrame)
- Professional PDF output: dark header, colored blocks, merged cells, legend, footer
- Sample JSON: "Zimní výjezd oddílu" with 11 blocks, 3 program types
- 30 tests passing (API, core models, PDF generation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 17:02:51 +01:00
parent e2bdadd0ce
commit 25fd578543
27 changed files with 2004 additions and 3016 deletions

View File

@@ -1,10 +1,7 @@
"""
API endpoint tests using FastAPI TestClient.
API endpoint tests for Scenar Creator v3.
"""
import io
import json
import pandas as pd
import pytest
from fastapi.testclient import TestClient
@@ -16,11 +13,20 @@ def client():
return TestClient(app)
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 make_valid_doc():
return {
"version": "1.0",
"event": {"title": "Test Event"},
"program_types": [{"id": "ws", "name": "Workshop", "color": "#FF0000"}],
"blocks": [{
"id": "b1",
"date": "2026-03-01",
"start": "09:00",
"end": "10:00",
"title": "Opening",
"type_id": "ws"
}]
}
def test_health(client):
@@ -28,28 +34,18 @@ def test_health(client):
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
assert data["version"] == "2.0.0"
assert data["version"] == "3.0.0"
def test_root_returns_html(client):
r = client.get("/")
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert "Scenar Creator" in r.text
assert "Scen" in r.text and "Creator" in r.text
def test_validate_valid(client):
doc = {
"event": {"title": "Test", "detail": "Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "09:00:00",
"konec": "10:00:00",
"program": "Opening",
"typ": "WS"
}]
}
doc = make_valid_doc()
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
data = r.json()
@@ -58,17 +54,8 @@ def test_validate_valid(client):
def test_validate_unknown_type(client):
doc = {
"event": {"title": "Test", "detail": "Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "09:00:00",
"konec": "10:00:00",
"program": "Opening",
"typ": "UNKNOWN"
}]
}
doc = make_valid_doc()
doc["blocks"][0]["type_id"] = "UNKNOWN"
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
data = r.json()
@@ -77,102 +64,81 @@ def test_validate_unknown_type(client):
def test_validate_bad_time_order(client):
doc = {
"event": {"title": "Test", "detail": "Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "10:00:00",
"konec": "09:00:00",
"program": "Bad",
"typ": "WS"
}]
}
doc = make_valid_doc()
doc["blocks"][0]["start"] = "10:00"
doc["blocks"][0]["end"] = "09:00"
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
data = r.json()
assert data["valid"] is False
assert any("start time" in e for e in data["errors"])
def test_validate_no_blocks(client):
doc = make_valid_doc()
doc["blocks"] = []
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
data = r.json()
assert data["valid"] is False
def test_import_excel(client):
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': ['09:00'],
'Konec': ['10:00'],
'Program': ['Test Program'],
'Typ': ['WORKSHOP'],
'Garant': ['John'],
'Poznamka': ['Note']
})
content = make_excel_bytes(df)
r = client.post(
"/api/import-excel",
files={"file": ("test.xlsx", content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
data={"title": "Imported Event", "detail": "From Excel"}
)
def test_validate_no_types(client):
doc = make_valid_doc()
doc["program_types"] = []
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
data = r.json()
assert data["success"] is True
assert data["document"] is not None
assert data["document"]["event"]["title"] == "Imported Event"
assert len(data["document"]["blocks"]) == 1
assert data["valid"] is False
def test_generate_excel(client):
doc = {
"event": {"title": "Test Event", "detail": "Test Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#0070C0"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "09:00:00",
"konec": "10:00:00",
"program": "Opening",
"typ": "WS",
"garant": "John",
"poznamka": "Note"
}]
}
r = client.post("/api/generate-excel", json=doc)
def test_sample_endpoint(client):
r = client.get("/api/sample")
assert r.status_code == 200
assert "spreadsheetml" in r.headers["content-type"]
assert len(r.content) > 0
data = r.json()
assert data["version"] == "1.0"
assert data["event"]["title"] == "Zimní výjezd oddílu"
assert len(data["program_types"]) == 3
assert len(data["blocks"]) >= 8
def test_generate_excel_no_blocks(client):
doc = {
"event": {"title": "Test", "detail": "Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
"blocks": []
}
r = client.post("/api/generate-excel", json=doc)
def test_sample_blocks_valid(client):
r = client.get("/api/sample")
data = r.json()
type_ids = {pt["id"] for pt in data["program_types"]}
for block in data["blocks"]:
assert block["type_id"] in type_ids
assert block["start"] < block["end"]
def test_generate_pdf(client):
doc = make_valid_doc()
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 200
assert r.headers["content-type"] == "application/pdf"
assert r.content[:5] == b'%PDF-'
def test_generate_pdf_no_blocks(client):
doc = make_valid_doc()
doc["blocks"] = []
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 422
def test_export_json(client):
doc = {
"event": {"title": "Test", "detail": "Detail"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "09:00:00",
"konec": "10:00:00",
"program": "Opening",
"typ": "WS"
}]
}
r = client.post("/api/export-json", json=doc)
def test_generate_pdf_multiday(client):
doc = make_valid_doc()
doc["blocks"].append({
"id": "b2",
"date": "2026-03-02",
"start": "14:00",
"end": "15:00",
"title": "Day 2 Session",
"type_id": "ws"
})
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 200
data = r.json()
assert data["event"]["title"] == "Test"
assert len(data["blocks"]) == 1
def test_template_download(client):
r = client.get("/api/template")
# Template might not exist in test env, but endpoint should work
assert r.status_code in [200, 404]
assert r.content[:5] == b'%PDF-'
def test_swagger_docs(client):

View File

@@ -1,532 +1,97 @@
"""
Core business logic tests — adapted from original test_read_excel.py and test_inline_builder.py.
Tests the refactored app.core modules.
Core logic tests for Scenar Creator v3.
Tests models, validation, and document structure.
"""
import io
import pandas as pd
import pytest
from datetime import date, time
from pydantic import ValidationError as PydanticValidationError
from app.core import (
read_excel, create_timetable, get_program_types, ScenarsError,
parse_inline_schedule, parse_inline_types, ValidationError,
validate_inputs, normalize_time,
)
from app.models.event import Block, ProgramType, EventInfo, ScenarioDocument
from app.core.validator import ScenarsError, 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()
# --- Model tests ---
def test_block_default_id():
b = Block(date="2026-03-01", start="09:00", end="10:00", title="Test", type_id="ws")
assert b.id is not None
assert len(b.id) > 0
def test_block_optional_fields():
b = Block(date="2026-03-01", start="09:00", end="10:00", title="Test", type_id="ws")
assert b.responsible is None
assert b.notes is None
def test_block_with_all_fields():
b = Block(
id="custom-id", date="2026-03-01", start="09:00", end="10:00",
title="Full Block", type_id="ws", responsible="John", notes="A note"
)
assert b.id == "custom-id"
assert b.responsible == "John"
assert b.notes == "A note"
def test_program_type():
pt = ProgramType(id="main", name="Main Program", color="#3B82F6")
assert pt.id == "main"
assert pt.name == "Main Program"
assert pt.color == "#3B82F6"
def test_event_info_minimal():
e = EventInfo(title="Test")
assert e.title == "Test"
assert e.subtitle is None
assert e.date is None
assert e.location is None
def test_event_info_full():
e = EventInfo(title="Event", subtitle="Sub", date="2026-03-01", location="Prague")
assert e.location == "Prague"
def test_scenario_document():
doc = ScenarioDocument(
event=EventInfo(title="Test"),
program_types=[ProgramType(id="ws", name="Workshop", color="#FF0000")],
blocks=[Block(date="2026-03-01", start="09:00", end="10:00", title="B1", type_id="ws")]
)
assert doc.version == "1.0"
assert len(doc.blocks) == 1
assert len(doc.program_types) == 1
def test_scenario_document_serialization():
doc = ScenarioDocument(
event=EventInfo(title="Test"),
program_types=[ProgramType(id="ws", name="Workshop", color="#FF0000")],
blocks=[Block(id="b1", date="2026-03-01", start="09:00", end="10:00", title="B1", type_id="ws")]
)
data = doc.model_dump(mode="json")
assert data["event"]["title"] == "Test"
assert data["blocks"][0]["type_id"] == "ws"
assert data["blocks"][0]["id"] == "b1"
def test_scenario_document_missing_title():
with pytest.raises(PydanticValidationError):
ScenarioDocument(
event=EventInfo(),
program_types=[],
blocks=[]
)
# --- Validator tests ---
def test_validate_inputs_valid():
validate_inputs("Title", "Detail", 100)
def test_scenars_error_hierarchy():
assert issubclass(ValidationError, ScenarsError)
def test_validate_inputs_empty_title():
with pytest.raises(ValidationError):
validate_inputs("", "Detail", 100)
def test_validate_inputs_long_title():
with pytest.raises(ValidationError):
validate_inputs("x" * 201, "Detail", 100)
def test_validate_inputs_file_too_large():
with pytest.raises(ValidationError):
validate_inputs("Title", "Detail", 11 * 1024 * 1024)
def test_normalize_time_hhmm():
t = normalize_time("09:00")
assert t == time(9, 0)
def test_normalize_time_hhmmss():
t = normalize_time("09:00:00")
assert t == time(9, 0)
def test_normalize_time_invalid():
assert normalize_time("invalid") is None
# --- Excel reader tests ---
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():
form_data = {
'type_code_0': 'WORKSHOP',
'type_code_1': 'LECTURE',
'desc_0': 'Workshop description',
'desc_1': 'Lecture description',
'color_0': '#FF0000',
'color_1': '#00FF00',
}
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'
assert colors['LECTURE'] == 'FF00FF00'
# --- Timetable tests ---
def test_create_timetable():
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': [time(9, 0)],
'Konec': [time(10, 0)],
'Program': ['Test Program'],
'Typ': ['WORKSHOP'],
'Garant': ['John Doe'],
'Poznamka': ['Test note'],
})
program_descriptions = {'WORKSHOP': 'Workshop Type'}
program_colors = {'WORKSHOP': 'FF0070C0'}
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():
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': [time(9, 0)],
'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"
# --- Inline schedule tests ---
def test_parse_inline_schedule():
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():
form_data = {
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
}
with pytest.raises(ValidationError):
parse_inline_schedule(form_data)
def test_parse_inline_types():
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'
# --- Integration tests ---
def test_inline_workflow_integration():
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',
}
types_form = {
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Keynote Speech',
'type_color_0': '#FF0000',
}
form_data = {**schedule_form, **types_form}
df = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
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():
import base64
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)
valid_data, errors = read_excel(file_content)
assert len(errors) == 0
assert len(valid_data) == 1
program_types = sorted([str(t).strip() for t in valid_data["Typ"].dropna().unique()])
assert program_types == ['KEYNOTE']
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'
}
descriptions, colors = get_program_types(form_data)
assert descriptions['KEYNOTE'] == 'Main keynote presentation'
assert colors['KEYNOTE'] == 'FFFF0000'
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():
import base64
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)
valid_data, errors = read_excel(file_content)
assert len(errors) == 0
assert len(valid_data) == 2
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
file_content_decoded = base64.b64decode(file_content_base64)
data_in_editor, _ = read_excel(file_content_decoded)
assert len(data_in_editor) == 2
assert data_in_editor.iloc[0]['Program'] == 'Morning Session'
assert data_in_editor.iloc[1]['Program'] == 'Afternoon Workshop'
program_types = sorted([str(t).strip() for t in data_in_editor["Typ"].dropna().unique()])
assert set(program_types) == {'KEYNOTE', 'WORKSHOP'}
# --- Inline builder tests (from test_inline_builder.py) ---
def test_inline_builder_valid_form():
form_data = {
'title': 'Test Conference',
'detail': 'Testing inline builder',
'step': 'builder',
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Opening Keynote',
'typ_0': 'KEYNOTE',
'garant_0': 'Dr. Smith',
'poznamka_0': 'Welcome speech',
'datum_1': '2025-11-13',
'zacatek_1': '10:30',
'konec_1': '11:30',
'program_1': 'Workshop: Python',
'typ_1': 'WORKSHOP',
'garant_1': 'Jane Doe',
'poznamka_1': 'Hands-on coding',
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Main Address',
'type_color_0': '#FF0000',
'type_name_1': 'WORKSHOP',
'type_desc_1': 'Interactive Session',
'type_color_1': '#0070C0',
}
schedule = parse_inline_schedule(form_data)
assert len(schedule) == 2
assert schedule.iloc[0]['Program'] == 'Opening Keynote'
descriptions, colors = parse_inline_types(form_data)
assert len(descriptions) == 2
assert descriptions['KEYNOTE'] == 'Main Address'
assert colors['KEYNOTE'] == 'FFFF0000'
wb = create_timetable(
schedule, 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 Conference'
def test_inline_builder_missing_type_definition():
form_data = {
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Unknown Program',
'typ_0': 'UNKNOWN_TYPE',
'garant_0': 'Someone',
'poznamka_0': '',
'type_name_0': 'LECTURE',
'type_desc_0': 'Standard Lecture',
'type_color_0': '#3498db',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
with pytest.raises(Exception):
create_timetable(schedule, 'Bad', 'Bad', descriptions, colors)
def test_inline_builder_empty_type_definition():
form_data = {
'type_name_0': '',
'type_desc_0': 'Empty type',
'type_color_0': '#FF0000',
'type_name_1': 'WORKSHOP',
'type_desc_1': 'Workshop Type',
'type_color_1': '#0070C0',
}
descriptions, colors = parse_inline_types(form_data)
assert 'WORKSHOP' in descriptions
assert '' not in descriptions
def test_inline_builder_overlapping_times():
form_data = {
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Program A',
'typ_0': 'WORKSHOP',
'garant_0': 'John',
'poznamka_0': '',
'datum_1': '2025-11-13',
'zacatek_1': '09:30',
'konec_1': '10:30',
'program_1': 'Program B',
'typ_1': 'WORKSHOP',
'garant_1': 'Jane',
'poznamka_1': '',
'type_name_0': 'WORKSHOP',
'type_desc_0': 'Workshop',
'type_color_0': '#0070C0',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
assert len(schedule) == 2
wb = create_timetable(schedule, 'Conf', 'Test', descriptions, colors)
assert wb is not None
def test_inline_builder_multiday():
form_data = {
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Day 1 Opening',
'typ_0': 'KEYNOTE',
'garant_0': 'Dr. A',
'poznamka_0': '',
'datum_1': '2025-11-14',
'zacatek_1': '09:00',
'konec_1': '10:00',
'program_1': 'Day 2 Opening',
'typ_1': 'KEYNOTE',
'garant_1': 'Dr. B',
'poznamka_1': '',
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Keynote Speech',
'type_color_0': '#FF6600',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
wb = create_timetable(schedule, 'Multi-day', 'Test', descriptions, colors)
assert wb is not None
ws = wb.active
assert ws['A1'].value == 'Multi-day'
def test_inline_builder_validation_errors():
form_data_missing = {
'datum_0': '2025-11-13',
'garant_0': 'John',
}
with pytest.raises(ValidationError):
parse_inline_schedule(form_data_missing)
def test_inline_builder_with_empty_rows():
form_data = {
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Program 1',
'typ_0': 'WORKSHOP',
'garant_0': 'John',
'poznamka_0': '',
'datum_1': '',
'zacatek_1': '',
'konec_1': '',
'program_1': '',
'typ_1': '',
'garant_1': '',
'poznamka_1': '',
'type_name_0': 'WORKSHOP',
'type_desc_0': 'Workshop',
'type_color_0': '#0070C0',
}
schedule = parse_inline_schedule(form_data)
assert len(schedule) == 1
assert schedule.iloc[0]['Program'] == 'Program 1'
def test_validation_error_message():
err = ValidationError("test error")
assert str(err) == "test error"

View File

@@ -1,61 +0,0 @@
import os
import shutil
import subprocess
import time
import urllib.request
import pytest
def podman_available():
return shutil.which("podman") is not None
def wait_for_http(url, timeout=30):
end = time.time() + timeout
last_exc = None
while time.time() < end:
try:
with urllib.request.urlopen(url, timeout=3) as r:
body = r.read().decode('utf-8', errors='ignore')
return r.getcode(), body
except Exception as e:
last_exc = e
time.sleep(0.5)
raise RuntimeError(f"HTTP check failed after {timeout}s: {last_exc}")
@pytest.mark.skipif(not podman_available(), reason="Podman is not available on this runner")
@pytest.mark.integration
def test_build_run_and_cleanup_podman():
image_tag = "scenar-creator:latest"
container_name = "scenar-creator-test"
port = int(os.environ.get('SCENAR_TEST_PORT', '8080'))
# Ensure podman machine is running (macOS/Windows)
if not subprocess.run(["podman", "machine", "info"], capture_output=True).returncode == 0:
print("Starting podman machine...")
subprocess.run(["podman", "machine", "start"], check=True)
time.sleep(2)
# Build image
subprocess.run(["podman", "build", "-t", image_tag, "."], check=True)
# Ensure no leftover container
subprocess.run(["podman", "rm", "-f", container_name], check=False)
try:
# Run container
subprocess.run([
"podman", "run", "-d", "--name", container_name, "-p", f"{port}:8080", image_tag
], check=True)
# Wait for HTTP and verify content
code, body = wait_for_http(f"http://127.0.0.1:{port}/")
assert code == 200
assert "Vytvoření Scénáře" in body or "Scenar" in body or "Vytvoření" in body
finally:
# Cleanup container and image
subprocess.run(["podman", "rm", "-f", container_name], check=False)
subprocess.run(["podman", "rmi", image_tag], check=False)

View File

@@ -1,138 +0,0 @@
"""
HTTP/CGI integration tests for inline builder workflow.
Tests the real form submission through the CGI endpoint.
"""
import pytest
import urllib.parse
import subprocess
import json
from io import BytesIO
def send_cgi_request(post_data=None, get_params=None):
"""
Send HTTP request to local CGI server and return response.
Assumes server is running on localhost:8000
"""
import urllib.request
import urllib.error
url = "http://localhost:8000/cgi-bin/scenar.py"
if get_params:
url += "?" + urllib.parse.urlencode(get_params)
if post_data:
data = urllib.parse.urlencode(post_data).encode('utf-8')
else:
data = None
try:
req = urllib.request.Request(url, data=data, method='POST' if data else 'GET')
with urllib.request.urlopen(req, timeout=10) as response:
return response.read().decode('utf-8'), response.status
except Exception as e:
return str(e), None
@pytest.mark.integration
def test_inline_builder_form_submission_valid():
"""Test inline builder with valid form data."""
form_data = {
'title': 'Test Event',
'detail': 'Test Detail',
'step': 'builder',
# Schedule row 0
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Opening',
'typ_0': 'KEYNOTE',
'garant_0': 'John Smith',
'poznamka_0': 'Welcome',
# Type definition 0
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Main keynote',
'type_color_0': '#FF0000',
}
response, status = send_cgi_request(post_data=form_data)
# Should get 200 OK
assert status == 200, f"Expected 200, got {status}"
# Response should contain success message
assert '✅ Scénář úspěšně vygenerován' in response or 'Stáhnout scénář' in response, \
f"Expected success message in response, got: {response[:500]}"
# Should have download link
assert '/tmp/' in response, "Expected download link in response"
@pytest.mark.integration
def test_inline_builder_missing_type():
"""Test that missing type definition is rejected."""
form_data = {
'title': 'Test Event',
'detail': 'Test Detail',
'step': 'builder',
# Schedule with type WORKSHOP
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Workshop',
'typ_0': 'WORKSHOP', # This type is not defined!
'garant_0': 'John',
'poznamka_0': '',
# Only KEYNOTE defined
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Keynote',
'type_color_0': '#FF0000',
}
response, status = send_cgi_request(post_data=form_data)
# Should get error response
assert status == 200 # CGI returns 200 with error content
assert 'Chyba' in response or 'Missing type' in response, \
f"Expected error message, got: {response[:500]}"
@pytest.mark.integration
def test_home_page_load():
"""Test that home page loads without errors."""
response, status = send_cgi_request()
assert status == 200
assert '<!DOCTYPE html>' in response
assert 'Scenar Creator' in response
assert 'Importovat Excel' in response # Tab 1
assert 'Vytvořit inline' in response # Tab 2
@pytest.mark.integration
def test_inline_editor_tabs_present():
"""Test that inline editor form elements are present."""
response, status = send_cgi_request()
assert status == 200
# Check for key form elements
assert 'id="builderForm"' in response
assert 'id="scheduleTable"' in response
assert 'id="typesContainer"' in response
assert 'id="availableTypes"' in response # datalist
# Check for JavaScript functions
assert 'function addScheduleRow' in response
assert 'function removeScheduleRow' in response
assert 'function addTypeRow' in response
assert 'function removeTypeRow' in response
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -1,261 +0,0 @@
"""
End-to-end tests for inline builder (without Excel upload).
Tests the full form submission flow from HTML form to timetable generation.
"""
import pytest
from datetime import date, time
import pandas as pd
from scenar.core import parse_inline_schedule, parse_inline_types, create_timetable, ValidationError
def test_inline_builder_valid_form():
"""Test inline builder with valid schedule and types."""
# Simulate form data from HTML
form_data = {
# Metadata
'title': 'Test Conference',
'detail': 'Testing inline builder',
'step': 'builder',
# Schedule rows (2 rows)
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Opening Keynote',
'typ_0': 'KEYNOTE',
'garant_0': 'Dr. Smith',
'poznamka_0': 'Welcome speech',
'datum_1': '2025-11-13',
'zacatek_1': '10:30',
'konec_1': '11:30',
'program_1': 'Workshop: Python',
'typ_1': 'WORKSHOP',
'garant_1': 'Jane Doe',
'poznamka_1': 'Hands-on coding',
# Type definitions
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Main Address',
'type_color_0': '#FF0000',
'type_name_1': 'WORKSHOP',
'type_desc_1': 'Interactive Session',
'type_color_1': '#0070C0',
}
# Parse schedule
schedule = parse_inline_schedule(form_data)
assert len(schedule) == 2
assert schedule.iloc[0]['Program'] == 'Opening Keynote'
assert schedule.iloc[0]['Typ'] == 'KEYNOTE'
# Parse types
descriptions, colors = parse_inline_types(form_data)
assert len(descriptions) == 2
assert descriptions['KEYNOTE'] == 'Main Address'
assert colors['KEYNOTE'] == 'FFFF0000'
# Generate timetable
wb = create_timetable(
schedule,
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 Conference'
assert ws['A2'].value == 'Testing inline builder'
def test_inline_builder_missing_type_definition():
"""Test that undefined type in schedule raises error."""
form_data = {
'title': 'Bad Conference',
'detail': 'Missing type definition',
# Schedule with UNKNOWN type
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Unknown Program',
'typ_0': 'UNKNOWN_TYPE', # This type is not defined below!
'garant_0': 'Someone',
'poznamka_0': '',
# Only define LECTURE, not UNKNOWN_TYPE
'type_name_0': 'LECTURE',
'type_desc_0': 'Standard Lecture',
'type_color_0': '#3498db',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
# Should fail because UNKNOWN_TYPE is not in colors dict
with pytest.raises(Exception): # ScenarsError
create_timetable(schedule, 'Bad', 'Bad', descriptions, colors)
def test_inline_builder_empty_type_definition():
"""Test that empty type definitions are skipped."""
form_data = {
'title': 'Conference',
'detail': 'With empty types',
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Program 1',
'typ_0': 'WORKSHOP',
'garant_0': 'John',
'poznamka_0': '',
# One empty type definition (should be skipped)
'type_name_0': '',
'type_desc_0': 'Empty type',
'type_color_0': '#FF0000',
# One valid type
'type_name_1': 'WORKSHOP',
'type_desc_1': 'Workshop Type',
'type_color_1': '#0070C0',
}
descriptions, colors = parse_inline_types(form_data)
# Empty type should be skipped
assert 'WORKSHOP' in descriptions
assert '' not in descriptions
def test_inline_builder_overlapping_times():
"""Test schedule with overlapping time slots."""
form_data = {
'title': 'Conference',
'detail': 'Overlapping schedule',
# Two programs at same time
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Program A',
'typ_0': 'WORKSHOP',
'garant_0': 'John',
'poznamka_0': '',
'datum_1': '2025-11-13',
'zacatek_1': '09:30', # Overlaps with Program A
'konec_1': '10:30',
'program_1': 'Program B',
'typ_1': 'WORKSHOP',
'garant_1': 'Jane',
'poznamka_1': '',
'type_name_0': 'WORKSHOP',
'type_desc_0': 'Workshop',
'type_color_0': '#0070C0',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
# Should allow overlapping times — timetable will show them
assert len(schedule) == 2
# Generate timetable (may have rendering issues but should complete)
wb = create_timetable(schedule, 'Conf', 'Test', descriptions, colors)
assert wb is not None
def test_inline_builder_multiday():
"""Test schedule spanning multiple days."""
form_data = {
'title': 'Multi-day Conference',
'detail': 'Two-day event',
# Day 1
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Day 1 Opening',
'typ_0': 'KEYNOTE',
'garant_0': 'Dr. A',
'poznamka_0': '',
# Day 2
'datum_1': '2025-11-14',
'zacatek_1': '09:00',
'konec_1': '10:00',
'program_1': 'Day 2 Opening',
'typ_1': 'KEYNOTE',
'garant_1': 'Dr. B',
'poznamka_1': '',
'type_name_0': 'KEYNOTE',
'type_desc_0': 'Keynote Speech',
'type_color_0': '#FF6600',
}
schedule = parse_inline_schedule(form_data)
descriptions, colors = parse_inline_types(form_data)
wb = create_timetable(schedule, 'Multi-day', 'Test', descriptions, colors)
assert wb is not None
ws = wb.active
# Should have multiple date rows
assert ws['A1'].value == 'Multi-day'
def test_inline_builder_validation_errors():
"""Test validation of inline form data."""
# Missing required field
form_data_missing = {
'datum_0': '2025-11-13',
# Missing zacatek_0, konec_0, program_0, typ_0
'garant_0': 'John',
}
with pytest.raises(ValidationError):
parse_inline_schedule(form_data_missing)
def test_inline_builder_with_empty_rows():
"""Test that empty schedule rows are skipped."""
form_data = {
'title': 'Test',
'detail': 'With empty rows',
# Valid row
'datum_0': '2025-11-13',
'zacatek_0': '09:00',
'konec_0': '10:00',
'program_0': 'Program 1',
'typ_0': 'WORKSHOP',
'garant_0': 'John',
'poznamka_0': '',
# Empty row (all fields missing)
'datum_1': '',
'zacatek_1': '',
'konec_1': '',
'program_1': '',
'typ_1': '',
'garant_1': '',
'poznamka_1': '',
'type_name_0': 'WORKSHOP',
'type_desc_0': 'Workshop',
'type_color_0': '#0070C0',
}
schedule = parse_inline_schedule(form_data)
# Should only have 1 row (empty row skipped)
assert len(schedule) == 1
assert schedule.iloc[0]['Program'] == 'Program 1'

View File

@@ -1,100 +1,95 @@
"""
PDF generation tests.
PDF generation tests for Scenar Creator v3.
"""
import pandas as pd
import pytest
from datetime import time
from fastapi.testclient import TestClient
from app.core.pdf_generator import generate_pdf
from app.core.validator import ScenarsError
from app.main import app
from app.models.event import ScenarioDocument, Block, ProgramType, EventInfo
@pytest.fixture
def client():
return TestClient(app)
def make_doc(**kwargs):
defaults = {
"version": "1.0",
"event": EventInfo(title="Test PDF", subtitle="Subtitle"),
"program_types": [
ProgramType(id="ws", name="Workshop", color="#0070C0"),
],
"blocks": [
Block(id="b1", date="2026-03-01", start="09:00", end="10:00",
title="Test Program", type_id="ws", responsible="John"),
]
}
defaults.update(kwargs)
return ScenarioDocument(**defaults)
def test_generate_pdf_basic():
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': [time(9, 0)],
'Konec': [time(10, 0)],
'Program': ['Test Program'],
'Typ': ['WORKSHOP'],
'Garant': ['John Doe'],
'Poznamka': ['Test note'],
})
descriptions = {'WORKSHOP': 'Workshop Type'}
colors = {'WORKSHOP': 'FF0070C0'}
pdf_bytes = generate_pdf(df, "Test PDF", "PDF Detail", descriptions, colors)
doc = make_doc()
pdf_bytes = generate_pdf(doc)
assert isinstance(pdf_bytes, bytes)
assert len(pdf_bytes) > 0
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_multiday():
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date(), pd.Timestamp('2025-11-14').date()],
'Zacatek': [time(9, 0), time(14, 0)],
'Konec': [time(10, 0), time(15, 0)],
'Program': ['Day 1', 'Day 2'],
'Typ': ['KEYNOTE', 'WORKSHOP'],
'Garant': ['Alice', 'Bob'],
'Poznamka': [None, 'Hands-on'],
})
descriptions = {'KEYNOTE': 'Keynote', 'WORKSHOP': 'Workshop'}
colors = {'KEYNOTE': 'FFFF0000', 'WORKSHOP': 'FF0070C0'}
pdf_bytes = generate_pdf(df, "Multi-day", "Two days", descriptions, colors)
doc = make_doc(
program_types=[
ProgramType(id="key", name="Keynote", color="#FF0000"),
ProgramType(id="ws", name="Workshop", color="#0070C0"),
],
blocks=[
Block(id="b1", date="2026-03-01", start="09:00", end="10:00",
title="Day 1", type_id="key", responsible="Alice"),
Block(id="b2", date="2026-03-02", start="14:00", end="15:00",
title="Day 2", type_id="ws", responsible="Bob"),
]
)
pdf_bytes = generate_pdf(doc)
assert isinstance(pdf_bytes, bytes)
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_empty_data():
df = pd.DataFrame(columns=['Datum', 'Zacatek', 'Konec', 'Program', 'Typ', 'Garant', 'Poznamka'])
def test_generate_pdf_empty_blocks():
doc = make_doc(blocks=[])
with pytest.raises(ScenarsError):
generate_pdf(df, "Empty", "Detail", {}, {})
generate_pdf(doc)
def test_generate_pdf_missing_type():
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': [time(9, 0)],
'Konec': [time(10, 0)],
'Program': ['Test'],
'Typ': ['UNKNOWN'],
'Garant': [None],
'Poznamka': [None],
})
doc = make_doc(
blocks=[
Block(id="b1", date="2026-03-01", start="09:00", end="10:00",
title="Test", type_id="UNKNOWN"),
]
)
with pytest.raises(ScenarsError):
generate_pdf(df, "Test", "Detail", {}, {})
generate_pdf(doc)
def test_generate_pdf_api(client):
doc = {
"event": {"title": "PDF Test", "detail": "API PDF"},
"program_types": [{"code": "WS", "description": "Workshop", "color": "#0070C0"}],
"blocks": [{
"datum": "2025-11-13",
"zacatek": "09:00:00",
"konec": "10:00:00",
"program": "Opening",
"typ": "WS",
"garant": "John",
"poznamka": "Note"
}]
}
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 200
assert r.headers["content-type"] == "application/pdf"
assert r.content[:5] == b'%PDF-'
def test_generate_pdf_with_event_info():
doc = make_doc(
event=EventInfo(
title="Full Event",
subtitle="With all fields",
date="2026-03-01",
location="Prague"
)
)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_multiple_blocks_same_day():
doc = make_doc(
blocks=[
Block(id="b1", date="2026-03-01", start="09:00", end="10:00",
title="Morning", type_id="ws"),
Block(id="b2", date="2026-03-01", start="10:00", end="11:30",
title="Midday", type_id="ws"),
Block(id="b3", date="2026-03-01", start="14:00", end="16:00",
title="Afternoon", type_id="ws", responsible="Team"),
]
)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'

View File

@@ -1,361 +0,0 @@
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'}