#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Scenar Creator — CGI web application for creating timetables from Excel data. Main UI logic and HTTP handling. """ import sys import os # Fallback sys.path for CGI context (add both parent and current directory) DOCROOT = "/var/www/htdocs" SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PARENT_DIR = os.path.dirname(SCRIPT_DIR) # Try multiple paths for package resolution for path in [DOCROOT, PARENT_DIR, SCRIPT_DIR]: if path not in sys.path: sys.path.insert(0, path) import html import base64 import logging import tempfile import urllib.parse from io import BytesIO import pandas as pd from scenar.core import ( read_excel, create_timetable, get_program_types, validate_inputs, ScenarsError, ValidationError, TemplateError, parse_inline_schedule, parse_inline_types ) # ===== Config ===== # Determine DOCROOT based on environment _default_docroot = "/var/www/htdocs" DOCROOT = _default_docroot # Try to use default, but fall back if permissions fail try: # Try to use /var/www/htdocs if in production if os.path.exists("/var/www"): os.makedirs(_default_docroot, exist_ok=True) DOCROOT = _default_docroot else: # Local dev: use current directory DOCROOT = os.getcwd() if "pytest" in sys.modules: # In tests: use temp directory DOCROOT = tempfile.gettempdir() except (OSError, PermissionError): # If can't use /var/www, use current directory or temp DOCROOT = os.getcwd() if "pytest" in sys.modules or not os.access(DOCROOT, os.W_OK): DOCROOT = tempfile.gettempdir() TMP_DIR = os.path.join(DOCROOT, "tmp") DEFAULT_COLOR = "#ffffff" MAX_FILE_SIZE_MB = 10 # =================== # Setup logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) print("Content-Type: text/html; charset=utf-8") print() # Simple CGI form parser (replaces deprecated cgi.FieldStorage for Python 3.13+) class FileItem: """Mimics cgi.FieldStorage file item for multipart uploads.""" def __init__(self, filename, content): self.filename = filename self.file = BytesIO(content) self.value = content class SimpleFieldStorage(dict): """Simple dictionary-based form parser for GET/POST data and file uploads.""" def __init__(self): super().__init__() self._parse_form() def _parse_form(self): """Parse GET/POST parameters into dict, including multipart file uploads.""" # Parse GET parameters query_string = os.environ.get('QUERY_STRING', '') if query_string: for key, value in urllib.parse.parse_qsl(query_string): self[key] = value # Parse POST parameters content_type = os.environ.get('CONTENT_TYPE', '') content_length_str = os.environ.get('CONTENT_LENGTH', '0').strip() content_length = int(content_length_str) if content_length_str else 0 if content_length == 0: return # Read body try: body = sys.stdin.buffer.read(content_length) except (AttributeError, TypeError): body_str = sys.stdin.read(content_length) body = body_str.encode('utf-8') if isinstance(body_str, str) else body_str if content_type.startswith('application/x-www-form-urlencoded'): # URL-encoded form for key, value in urllib.parse.parse_qsl(body.decode('utf-8')): self[key] = value elif content_type.startswith('multipart/form-data'): # Multipart form (file upload) boundary_match = content_type.split('boundary=') if len(boundary_match) > 1: boundary = boundary_match[1].split(';')[0].strip('"') self._parse_multipart(body, boundary) def _parse_multipart(self, body: bytes, boundary: str): """Parse multipart/form-data body.""" boundary_bytes = f'--{boundary}'.encode('utf-8') end_boundary = f'--{boundary}--'.encode('utf-8') parts = body.split(boundary_bytes) for part in parts: if part.startswith(b'--') or not part.strip(): continue # Split headers from content try: header_end = part.find(b'\r\n\r\n') if header_end == -1: header_end = part.find(b'\n\n') if header_end == -1: continue headers = part[:header_end].decode('utf-8', errors='ignore') content = part[header_end + 2:] else: headers = part[:header_end].decode('utf-8', errors='ignore') content = part[header_end + 4:] except: continue # Remove trailing boundary marker and whitespace if content.endswith(b'\r\n'): content = content[:-2] elif content.endswith(b'\n'): content = content[:-1] # Parse Content-Disposition header name = None filename = None for line in headers.split('\n'): if 'Content-Disposition:' in line: # Extract name and filename import re as regex name_match = regex.search(r'name="([^"]*)"', line) if name_match: name = name_match.group(1) filename_match = regex.search(r'filename="([^"]*)"', line) if filename_match: filename = filename_match.group(1) if name: if filename: # File field self[name] = FileItem(filename, content) else: # Regular form field self[name] = content.decode('utf-8', errors='ignore').strip() def __contains__(self, key): """Check if key exists (for 'in' operator).""" return super().__contains__(key) def getvalue(self, key, default=''): """Get value from form, mimicking cgi.FieldStorage API.""" val = self.get(key, default) if isinstance(val, FileItem): return val.value return val form = SimpleFieldStorage() title = form.getvalue('title', '').strip() detail = form.getvalue('detail', '').strip() show_debug = form.getvalue('debug') == 'on' step = form.getvalue('step', '1') file_item = form.get('file', None) # Get file upload if present def render_error(message: str) -> None: """Render error page.""" print(f''' Chyba - Scenar Creator
⚠️ Chyba

{html.escape(message)}

← Zpět na úvodní formulář

''') def render_home() -> None: """Render home page with import and builder tabs.""" print(''' Scenar Creator - Nový scénář

�� Scenar Creator

Importovat scénář z Excelu

📥 Stáhnout šablonu

Vytvořit scénář přímo

Program (řádky)

Datum Začátek Konec Program Typ Garant Poznámka Akce

Typy programu

''') # ====== Main flow ====== if step == '1': render_home() elif step == '2' and file_item is not None and file_item.filename: try: file_content = file_item.file.read() file_size = len(file_content) # Validate inputs validate_inputs(title, detail, file_size) # Read Excel data, error_rows = read_excel(file_content, show_debug) if data.empty: render_error("Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.") else: # Instead of showing type selection form, go directly to inline editor (step 2b) # Extract program types and prepare for inline editor program_types = sorted([str(t).strip() for t in data["Typ"].dropna().unique()]) # Render inline editor with loaded data print(''' Scenar Creator - Upravit scénář

Upravit scénář

Program (řádky)

''') # Load data from Excel into inline editor row_counter = 0 for _, row in data.iterrows(): datum = row['Datum'].strftime('%Y-%m-%d') if pd.notna(row['Datum']) else '' zacatek = str(row['Zacatek']).strip() if pd.notna(row['Zacatek']) else '' konec = str(row['Konec']).strip() if pd.notna(row['Konec']) else '' program = str(row['Program']).strip() if pd.notna(row['Program']) else '' typ = str(row['Typ']).strip() if pd.notna(row['Typ']) else '' garant = str(row.get('Garant', '')).strip() if pd.notna(row.get('Garant')) else '' poznamka = str(row.get('Poznamka', '')).strip() if pd.notna(row.get('Poznamka')) else '' print(f''' ''') row_counter += 1 print('''
Datum Začátek Konec Program Typ Garant Poznámka Akce

Typy programu (nastavení barev a popisů)

''') # Load type definitions type_counter = 0 for type_name in program_types: print(f'''
''') type_counter += 1 print('''
''') except ValidationError as e: render_error(f"Chyba validace: {str(e)}") except (TemplateError, ScenarsError) as e: render_error(f"Chyba: {str(e)}") except Exception as e: logger.error(f"Unexpected error: {str(e)}") render_error(f"Neočekávaná chyba: {str(e)}") elif step == 'builder': """Handle inline builder form submission.""" try: validate_inputs(title, detail, 0) # 0 = no file size check # Parse inline schedule and types data = parse_inline_schedule(form) program_descriptions, program_colors = parse_inline_types(form) # Generate timetable wb = create_timetable(data, title, detail, program_descriptions, program_colors) # Save to tmp os.makedirs(TMP_DIR, exist_ok=True) filename = f"{title}.xlsx" safe_name = "".join(ch if ch.isalnum() or ch in "._- " else "_" for ch in filename) file_path = os.path.join(TMP_DIR, safe_name) wb.save(file_path) print(f''' Scenar Creator - Výsledek

✅ Scénář úspěšně vygenerován!

{html.escape(title)}

{html.escape(detail)}

📥 Stáhnout scénář

← Zpět na úvodní formulář

''') except ValidationError as e: render_error(f"Chyba validace: {str(e)}") except ScenarsError as e: render_error(f"Chyba: {str(e)}") except Exception as e: logger.error(f"Unexpected error in builder: {str(e)}") render_error(f"Neočekávaná chyba: {str(e)}") else: render_error("Chyba: Neplatný krok nebo chybějící data.")