#!/usr/bin/env python3 # -*- coding: utf-8 -*- import cgi import cgitb import html import pandas as pd from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter from datetime import datetime from io import BytesIO import base64 import os # ===== Config ===== DOCROOT = "/var/www/htdocs" TMP_DIR = os.path.join(DOCROOT, "tmp") # soubory budou dostupné jako /tmp/ DEFAULT_COLOR = "#ffffff" # výchozí barva pro # =================== cgitb.enable() print("Content-Type: text/html; charset=utf-8") print() form = cgi.FieldStorage() title = form.getvalue('title') detail = form.getvalue('detail') show_debug = form.getvalue('debug') == 'on' step = form.getvalue('step', '1') file_item = form['file'] if 'file' in form else None def get_program_types(form): program_descriptions = {} program_colors = {} for key in form.keys(): if key.startswith('type_code_'): index = key.split('_')[-1] type_code = form.getvalue(f'type_code_{index}') description = form.getvalue(f'desc_{index}', '') raw_color = form.getvalue(f'color_{index}', DEFAULT_COLOR) or DEFAULT_COLOR color_hex = 'FF' + raw_color.lstrip('#') # openpyxl chce AARRGGBB program_descriptions[type_code] = description program_colors[type_code] = color_hex return program_descriptions, program_colors program_descriptions, program_colors = get_program_types(form) def normalize_time(time_str): for fmt in ('%H:%M', '%H:%M:%S'): try: return datetime.strptime(time_str, fmt).time() except ValueError: continue return None def read_excel(file_content): excel_data = pd.read_excel(BytesIO(file_content), skiprows=0) excel_data.columns = ["Datum", "Zacatek", "Konec", "Program", "Typ", "Garant", "Poznamka"] if show_debug: print("
Raw data:\n")
        print(excel_data.head())
        print("
") error_rows = [] valid_data = [] for index, row in excel_data.iterrows(): try: datum = pd.to_datetime(row["Datum"], errors='coerce').date() zacatek = normalize_time(str(row["Zacatek"])) konec = normalize_time(str(row["Konec"])) if pd.isna(datum) or zacatek is None or konec is None: raise ValueError("Invalid date or time format") valid_data.append({ "index": index, "Datum": datum, "Zacatek": zacatek, "Konec": konec, "Program": row["Program"], "Typ": row["Typ"], "Garant": row["Garant"], "Poznamka": row["Poznamka"], "row_data": row }) except Exception as e: error_rows.append({"index": index, "row": row, "error": str(e)}) valid_data = pd.DataFrame(valid_data) # Pokud nejsou žádné validní řádky, vrať prázdný DataFrame a chyby. if valid_data.empty: if show_debug: print("
No valid rows after parsing
") return valid_data.drop(columns='index', errors='ignore'), error_rows if show_debug: print("
Cleaned data:\n")
        print(valid_data.head())
        print("
") print("
Error rows:\n")
        for er in error_rows:
            print(f"Index: {er['index']}, Error: {er['error']}")
            print(er['row'])
        print("
") # Detekce překryvů overlap_errors = [] for date, group in valid_data.groupby('Datum'): sorted_group = group.sort_values(by='Zacatek') previous_end_time = None for _, r in sorted_group.iterrows(): if previous_end_time and r['Zacatek'] < previous_end_time: overlap_errors.append({ "index": r["index"], "Datum": r["Datum"], "Zacatek": r["Zacatek"], "Konec": r["Konec"], "Program": r["Program"], "Typ": r["Typ"], "Garant": r["Garant"], "Poznamka": r["Poznamka"], "Error": f"Overlapping time block with previous block ending at {previous_end_time}", "row_data": r["row_data"] }) previous_end_time = r['Konec'] if overlap_errors: if show_debug: print("
Overlap errors:\n")
            for e in overlap_errors:
                print(e)
            print("
") valid_data = valid_data[~valid_data.index.isin([e['index'] for e in overlap_errors])] error_rows.extend(overlap_errors) return valid_data.drop(columns='index'), error_rows def calculate_row_height(cell_value, column_width): if not cell_value: return 15 max_line_length = column_width * 1.2 lines = str(cell_value).split('\n') line_count = 0 for line in lines: line_count += len(line) // max_line_length + 1 return line_count * 15 def calculate_column_width(text): max_length = max(len(line) for line in str(text).split('\n')) return max_length * 1.2 def create_timetable(data, title, detail, program_descriptions, program_colors): if data.empty: print("

Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.

") return None missing_types = [typ for typ in data["Typ"].unique() if typ not in program_colors] if missing_types: print(f"

Chyba: Následující typy programu nejsou specifikovány: {', '.join(missing_types)}. Zkontrolujte vstupní soubor a formulář.

") return None wb = Workbook() ws = wb.active thick_border = Border(left=Side(style='thick', color='000000'), right=Side(style='thick', color='000000'), top=Side(style='thick', color='000000'), bottom=Side(style='thick', color='000000')) ws['A1'] = title ws['A1'].alignment = Alignment(horizontal="center", vertical="center") ws['A1'].font = Font(size=24, bold=True) ws['A1'].border = thick_border ws['A2'] = detail ws['A2'].alignment = Alignment(horizontal="center", vertical="center") ws['A2'].font = Font(size=16, italic=True) ws['A2'].border = thick_border # rozumný default šířky prvního sloupce if ws.column_dimensions[get_column_letter(1)].width is None: ws.column_dimensions[get_column_letter(1)].width = 40 title_row_height = calculate_row_height(title, ws.column_dimensions[get_column_letter(1)].width) detail_row_height = calculate_row_height(detail, ws.column_dimensions[get_column_letter(1)].width) ws.row_dimensions[1].height = title_row_height ws.row_dimensions[2].height = detail_row_height data = data.sort_values(by=["Datum", "Zacatek"]) start_times = data["Zacatek"] end_times = data["Konec"] if start_times.isnull().any() or end_times.isnull().any(): print("

Chyba: Načtená data obsahují neplatné hodnoty času. Zkontrolujte vstupní soubor.

") if show_debug: print("
Start times:\n")
            print(start_times)
            print("\nEnd times:\n")
            print(end_times)
            print("
") return None try: min_time = min(start_times) max_time = max(end_times) except ValueError as e: print(f"

Chyba při zjišťování minimálního a maximálního času: {e}

") if show_debug: print("
Start times:\n")
            print(start_times)
            print("\nEnd times:\n")
            print(end_times)
            print("
") return None time_slots = pd.date_range( datetime.combine(datetime.today(), min_time), datetime.combine(datetime.today(), max_time), freq='15min' ).time total_columns = len(time_slots) + 1 ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=total_columns) ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=total_columns) row_offset = 3 col_offset = 1 cell = ws.cell(row=row_offset, column=col_offset, value="Datum") cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") cell.alignment = Alignment(horizontal="center", vertical="center") cell.font = Font(bold=True) cell.border = thick_border for i, time_slot in enumerate(time_slots, start=col_offset + 1): cell = ws.cell(row=row_offset, column=i, value=time_slot.strftime("%H:%M")) cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") cell.alignment = Alignment(horizontal="center", vertical="center") cell.font = Font(bold=True) cell.border = thick_border current_row = row_offset + 1 grouped_data = data.groupby(data['Datum']) for date, group in grouped_data: day_name = date.strftime("%A") date_str = date.strftime(f"%d.%m {day_name}") cell = ws.cell(row=current_row, column=col_offset, value=date_str) cell.alignment = Alignment(horizontal="center", vertical="center") cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") cell.font = Font(bold=True, size=14) cell.border = thick_border for _, row in group.iterrows(): start_time = row["Zacatek"] end_time = row["Konec"] try: start_index = list(time_slots).index(start_time) + col_offset + 1 end_index = list(time_slots).index(end_time) + col_offset + 1 except ValueError as e: print(f"

Chyba při hledání indexu časového slotu: {e}

") if show_debug: print("
Start time: {}\nEnd time: {}
".format(start_time, end_time)) continue cell_value = f"{row['Program']}" if pd.notna(row['Garant']): cell_value += f"\n{row['Garant']}" if pd.notna(row['Poznamka']): cell_value += f"\n\n{row['Poznamka']}" try: ws.merge_cells(start_row=current_row, start_column=start_index, end_row=current_row, end_column=end_index - 1) cell = ws.cell(row=current_row, column=start_index) cell.value = cell_value except AttributeError as e: print(f"

Chyba: {str(e)}. Zkontrolujte vstupní data, která způsobují překrývající se časy bloků.

") if show_debug: print("
Overlapping block:\n{}
".format(row)) return None cell.alignment = Alignment(wrap_text=True, horizontal="center", vertical="center") lines = str(cell_value).split("\n") for idx, _ in enumerate(lines): if idx == 0: cell.font = Font(bold=True) elif idx == 1: cell.font = Font(bold=False) elif idx > 1 and pd.notna(row['Poznamka']): cell.font = Font(italic=True) cell.fill = PatternFill(start_color=program_colors[row["Typ"]], end_color=program_colors[row["Typ"]], fill_type="solid") cell.border = thick_border current_row += 1 legend_row = current_row + 2 legend_max_length = 0 ws.cell(row=legend_row, column=1, value="Legenda:").font = Font(bold=True) legend_row += 1 for typ, desc in program_descriptions.items(): legend_text = f"{desc} ({typ})" legend_cell = ws.cell(row=legend_row, column=1, value=legend_text) legend_cell.fill = PatternFill(start_color=program_colors[typ], fill_type="solid") legend_max_length = max(legend_max_length, calculate_column_width(legend_text)) legend_row += 1 ws.column_dimensions[get_column_letter(1)].width = legend_max_length for col in range(2, total_columns + 1): ws.column_dimensions[get_column_letter(col)].width = 15 for row in ws.iter_rows(min_row=1, max_row=current_row - 1, min_col=1, max_col=total_columns): for cell in row: cell.alignment = Alignment(wrap_text=True, horizontal="center", vertical="center") cell.border = thick_border for row in ws.iter_rows(min_row=1, max_row=current_row - 1): max_height = 0 for cell in row: if cell.value: height = calculate_row_height(cell.value, ws.column_dimensions[get_column_letter(cell.column)].width) if height > max_height: max_height = height ws.row_dimensions[row[0].row].height = max_height return wb # ====== HTML flow ====== if step == '1': print(''' Vytvoření Scénáře - Krok 1

Vytvoření Scénáře - Krok 1

STÁHNOUT ŠABLONU SCÉNÁŘE ZDE

''') elif step == '2' and file_item is not None and file_item.filename: file_content = file_item.file.read() file_content_base64 = base64.b64encode(file_content).decode('utf-8') data, error_rows = read_excel(file_content) if data.empty: print("

Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.

") else: program_types = data["Typ"].dropna().unique() program_types = [typ.strip() for typ in program_types] print(''' Vytvoření Scénáře - Krok 2

Vytvoření Scénáře - Krok 2

Vyplň tituly a barvy pro nalezené typy programu a odešli.

'''.format( title=html.escape(title or ""), detail=html.escape(detail or ""), fcb64=html.escape(file_content_base64)) ) for i, typ in enumerate(program_types, start=1): print('''
'''.format(i=i, typ=html.escape(typ), default_color=DEFAULT_COLOR)) print('''
'''.format(checked=' checked' if show_debug else '')) elif step == '3' and title and detail: file_content_base64 = form.getvalue('file_content_base64') if file_content_base64: file_content = base64.b64decode(file_content_base64) data, error_rows = read_excel(file_content) if data.empty: print("

Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.

") else: # z POSTu teď přijdou zvolené popisy a barvy program_descriptions, program_colors = get_program_types(form) wb = create_timetable(data, title, detail, program_descriptions, program_colors) if wb: 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('''

Výsledky zpracování

Stáhnout scénář pro {title} ZDE

Název akce: {title}

Detail akce: {detail}

Data ze souboru:

{table}

Stáhnout scénář pro {title} ZDE

'''.format( name=html.escape(safe_name), title=html.escape(title or ""), detail=html.escape(detail or ""), table=data.to_html(index=False))) else: print("

Chyba: Soubor nebyl nalezen. Zkontrolujte vstupní data.

") else: print("

Chyba: Není vybrán žádný soubor nebo došlo k chybě ve formuláři.

")