Refactor: Oddělení business logiky + inline editor
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
- Nový modul scenar/core.py (491 řádků čisté logiky)
- Refactored cgi-bin/scenar.py (450 řádků CGI wrapper)
- Inline editor s JavaScript row managementem
- Custom exceptions (ScenarsError, ValidationError, TemplateError)
- Kompletní test coverage (10 testů, všechny ✅)
- Fixed Dockerfile (COPY scenar/, requirements.txt)
- Fixed requirements.txt (openpyxl==3.1.5)
- Fixed pytest.ini (pythonpath = .)
- Nové testy: test_http_inline.py, test_inline_builder.py
- HTTP testy označeny jako @pytest.mark.integration
- Build script: scripts/build_image.sh
- Dokumentace: COMPLETION.md
This commit is contained in:
261
tests/test_inline_builder.py
Normal file
261
tests/test_inline_builder.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
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'
|
||||
Reference in New Issue
Block a user