feat: v4.0 - multi-day horizontal canvas, duration input, overnight blocks, PDF horizontal layout
Some checks failed
Build & Push Docker / build (push) Has been cancelled

This commit is contained in:
2026-02-20 17:31:41 +01:00
parent e3a5330cc2
commit 47add509ca
11 changed files with 1188 additions and 773 deletions

View File

@@ -1,5 +1,5 @@
"""
API endpoint tests for Scenar Creator v3.
API endpoint tests for Scenar Creator v4.
"""
import pytest
@@ -13,19 +13,33 @@ def client():
return TestClient(app)
def make_valid_doc():
def make_valid_doc(multiday=False):
blocks = [{
"id": "b1",
"date": "2026-03-01",
"start": "09:00",
"end": "10:00",
"title": "Opening",
"type_id": "ws"
}]
if multiday:
blocks.append({
"id": "b2",
"date": "2026-03-02",
"start": "10:00",
"end": "11:30",
"title": "Day 2 Session",
"type_id": "ws"
})
return {
"version": "1.0",
"event": {"title": "Test Event"},
"event": {
"title": "Test Event",
"date_from": "2026-03-01",
"date_to": "2026-03-02" if multiday else "2026-03-01"
},
"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"
}]
"blocks": blocks
}
@@ -34,7 +48,7 @@ def test_health(client):
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
assert data["version"] == "3.0.0"
assert data["version"] == "4.0.0"
def test_root_returns_html(client):
@@ -63,17 +77,6 @@ def test_validate_unknown_type(client):
assert any("UNKNOWN" in e for e in data["errors"])
def test_validate_bad_time_order(client):
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"] = []
@@ -98,17 +101,14 @@ def test_sample_endpoint(client):
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_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"]
assert len(data["program_types"]) >= 3
# multi-day sample
assert data["event"]["date_from"] == "2026-03-01"
assert data["event"]["date_to"] == "2026-03-02"
# blocks for both days
dates = {b["date"] for b in data["blocks"]}
assert "2026-03-01" in dates
assert "2026-03-02" in dates
def test_generate_pdf(client):
@@ -119,6 +119,23 @@ def test_generate_pdf(client):
assert r.content[:5] == b'%PDF-'
def test_generate_pdf_multiday(client):
doc = make_valid_doc(multiday=True)
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 200
assert r.content[:5] == b'%PDF-'
def test_generate_pdf_overnight_block(client):
"""Block that crosses midnight: end < start."""
doc = make_valid_doc()
doc["blocks"][0]["start"] = "23:00"
doc["blocks"][0]["end"] = "01:30" # overnight
r = client.post("/api/generate-pdf", json=doc)
assert r.status_code == 200
assert r.content[:5] == b'%PDF-'
def test_generate_pdf_no_blocks(client):
doc = make_valid_doc()
doc["blocks"] = []
@@ -126,21 +143,26 @@ def test_generate_pdf_no_blocks(client):
assert r.status_code == 422
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
assert r.content[:5] == b'%PDF-'
def test_swagger_docs(client):
r = client.get("/docs")
assert r.status_code == 200
def test_backward_compat_date_field(client):
"""Old JSON with 'date' (not date_from/date_to) should still validate."""
doc = {
"version": "1.0",
"event": {"title": "Old Format", "date": "2026-03-01"},
"program_types": [{"id": "t1", "name": "Type", "color": "#0000FF"}],
"blocks": [{
"id": "bx",
"date": "2026-03-01",
"start": "10:00",
"end": "11:00",
"title": "Session",
"type_id": "t1"
}]
}
r = client.post("/api/validate", json=doc)
assert r.status_code == 200
assert r.json()["valid"] is True

View File

@@ -1,5 +1,5 @@
"""
PDF generation tests for Scenar Creator v3.
PDF generation tests for Scenar Creator v4.
"""
import pytest
@@ -11,7 +11,8 @@ from app.models.event import ScenarioDocument, Block, ProgramType, EventInfo
def make_doc(**kwargs):
defaults = {
"version": "1.0",
"event": EventInfo(title="Test PDF", subtitle="Subtitle"),
"event": EventInfo(title="Test PDF", subtitle="Subtitle",
date_from="2026-03-01", date_to="2026-03-01"),
"program_types": [
ProgramType(id="ws", name="Workshop", color="#0070C0"),
],
@@ -32,8 +33,18 @@ def test_generate_pdf_basic():
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_is_single_page():
import re
doc = make_doc()
pdf_bytes = generate_pdf(doc)
# Count /Type /Page (not /Pages) occurrences
pages = len(re.findall(rb'/Type\s*/Page[^s]', pdf_bytes))
assert pages == 1, f"Expected 1 page, got {pages}"
def test_generate_pdf_multiday():
doc = make_doc(
event=EventInfo(title="Multi-day", date_from="2026-03-01", date_to="2026-03-02"),
program_types=[
ProgramType(id="key", name="Keynote", color="#FF0000"),
ProgramType(id="ws", name="Workshop", color="#0070C0"),
@@ -50,6 +61,18 @@ def test_generate_pdf_multiday():
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_overnight_block():
"""Block crossing midnight (end < start) should render without error."""
doc = make_doc(
blocks=[
Block(id="b1", date="2026-03-01", start="22:00", end="01:30",
title="Noční hra", type_id="ws", responsible="Honza"),
]
)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_empty_blocks():
doc = make_doc(blocks=[])
with pytest.raises(ScenarsError):
@@ -67,14 +90,27 @@ def test_generate_pdf_missing_type():
generate_pdf(doc)
def test_generate_pdf_with_event_info():
def test_generate_pdf_with_full_event_info():
doc = make_doc(
event=EventInfo(
title="Full Event",
subtitle="With all fields",
date="2026-03-01",
date_from="2026-03-01",
date_to="2026-03-03",
location="Prague"
)
),
program_types=[
ProgramType(id="ws", name="Workshop", color="#0070C0"),
ProgramType(id="rest", name="Odpočinek", color="#22C55E"),
],
blocks=[
Block(id="b1", date="2026-03-01", start="09:00", end="10:00",
title="Session 1", type_id="ws"),
Block(id="b2", date="2026-03-02", start="10:00", end="11:00",
title="Session 2", type_id="rest"),
Block(id="b3", date="2026-03-03", start="08:00", end="09:00",
title="Session 3", type_id="ws"),
]
)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'
@@ -93,3 +129,14 @@ def test_generate_pdf_multiple_blocks_same_day():
)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'
def test_generate_pdf_many_types_legend():
"""Many program types should fit in multi-column legend."""
types = [ProgramType(id=f"t{i}", name=f"Typ {i}", color=f"#{'%06x' % (i * 20000)}")
for i in range(1, 9)]
blocks = [Block(id=f"b{i}", date="2026-03-01", start=f"{8+i}:00", end=f"{9+i}:00",
title=f"Blok {i}", type_id=f"t{i}") for i in range(1, 9)]
doc = make_doc(program_types=types, blocks=blocks)
pdf_bytes = generate_pdf(doc)
assert pdf_bytes[:5] == b'%PDF-'