feat: v3.0 - canvas editor, JSON-only, no Excel, new UI
Some checks failed
Build & Push Docker / build (push) Has been cancelled
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:
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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'])
|
||||
@@ -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'
|
||||
@@ -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-'
|
||||
|
||||
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user