#!/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'''
{html.escape(message)}
Vyplň popis a barvu pro každý typ programu: