Files
scenar-creator/cgi-bin/scenar.py
Martin Sukany 9a7ffdeb2c copilot test
2025-11-13 11:37:28 +01:00

496 lines
20 KiB
Python
Executable File

#!/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/<soubor>
DEFAULT_COLOR = "#ffffff" # výchozí barva pro <input type="color">
# ===================
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("<pre>Raw data:\n")
print(excel_data.head())
print("</pre>")
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("<pre>No valid rows after parsing</pre>")
return valid_data.drop(columns='index', errors='ignore'), error_rows
if show_debug:
print("<pre>Cleaned data:\n")
print(valid_data.head())
print("</pre>")
print("<pre>Error rows:\n")
for er in error_rows:
print(f"Index: {er['index']}, Error: {er['error']}")
print(er['row'])
print("</pre>")
# 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("<pre>Overlap errors:\n")
for e in overlap_errors:
print(e)
print("</pre>")
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("<p>Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.</p>")
return None
missing_types = [typ for typ in data["Typ"].unique() if typ not in program_colors]
if missing_types:
print(f"<p>Chyba: Následující typy programu nejsou specifikovány: {', '.join(missing_types)}. Zkontrolujte vstupní soubor a formulář.</p>")
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("<p>Chyba: Načtená data obsahují neplatné hodnoty času. Zkontrolujte vstupní soubor.</p>")
if show_debug:
print("<pre>Start times:\n")
print(start_times)
print("\nEnd times:\n")
print(end_times)
print("</pre>")
return None
try:
min_time = min(start_times)
max_time = max(end_times)
except ValueError as e:
print(f"<p>Chyba při zjišťování minimálního a maximálního času: {e}</p>")
if show_debug:
print("<pre>Start times:\n")
print(start_times)
print("\nEnd times:\n")
print(end_times)
print("</pre>")
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"<p>Chyba při hledání indexu časového slotu: {e}</p>")
if show_debug:
print("<pre>Start time: {}\nEnd time: {}</pre>".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"<p>Chyba: {str(e)}. Zkontrolujte vstupní data, která způsobují překrývající se časy bloků.</p>")
if show_debug:
print("<pre>Overlapping block:\n{}</pre>".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('''<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Vytvoření Scénáře - Krok 1</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f0f4f8; color: #333; }
.form-container { max-width: 600px; margin: auto; padding: 20px; background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1); border-radius: 8px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; font-weight: bold; margin-bottom: 5px; color: #555; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; margin-top: 5px; }
.form-group input[type="color"] { width: auto; padding: 5px; }
.form-group button { padding: 10px 15px; border: none; background-color: #007BFF; color: white;
border-radius: 5px; cursor: pointer; }
.form-group button:hover { background-color: #0056b3; }
.footer { margin-top: 20px; text-align: center; font-size: 0.9em; color: #777; }
</style>
</head>
<body>
<div class="form-container">
<h1>Vytvoření Scénáře - Krok 1</h1>
<p><a href="/templates/scenar_template.xlsx">STÁHNOUT ŠABLONU SCÉNÁŘE ZDE</a></p>
<form action="/cgi-bin/scenar.py" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Název akce:</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="detail">Detail akce:</label>
<input type="text" id="detail" name="detail" required>
</div>
<div class="form-group">
<label for="file">Excel soubor:</label>
<input type="file" id="file" name="file" accept=".xlsx" required>
</div>
<div class="form-group">
<label for="debug">Zobrazit debug informace:</label>
<input type="checkbox" id="debug" name="debug">
</div>
<div class="form-group">
<input type="hidden" name="step" value="2">
<input type="submit" value="Načíst typy programu">
</div>
</form>
</div>
<div class="footer">
<p>© 2024 Martin Sukaný • <a href="mailto:martin@sukany.cz">martin@sukany.cz</a></p>
</div>
</body>
</html>''')
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("<p>Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.</p>")
else:
program_types = data["Typ"].dropna().unique()
program_types = [typ.strip() for typ in program_types]
print('''<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Vytvoření Scénáře - Krok 2</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f0f4f8; color: #333; }}
.form-container {{ max-width: 600px; margin: auto; padding: 20px; background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1); border-radius: 8px; }}
.form-group {{ margin-bottom: 15px; }}
.form-group label {{ display: block; font-weight: bold; margin-bottom: 5px; color: #555; }}
.form-group input, .form-group textarea, .form-group select {{
width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; margin-top: 5px; }}
.form-group input[type="color"] {{ width: auto; padding: 5px; }}
.form-group button {{ padding: 10px 15px; border: none; background-color: #007BFF; color: white;
border-radius: 5px; cursor: pointer; }}
.form-group button:hover {{ background-color: #0056b3; }}
.footer {{ margin-top: 20px; text-align: center; font-size: 0.9em; color: #777; }}
</style>
</head>
<body>
<div class="form-container">
<h1>Vytvoření Scénáře - Krok 2</h1>
<p>Vyplň tituly a barvy pro nalezené typy programu a odešli.</p>
<form action="/cgi-bin/scenar.py" method="post">
<input type="hidden" name="title" value="{title}">
<input type="hidden" name="detail" value="{detail}">
<input type="hidden" name="file_content_base64" value="{fcb64}">
<input type="hidden" name="step" value="3">'''.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('''
<div class="form-group program-type">
<label for="type_code_{i}">Typ programu {i}:</label>
<input type="text" id="type_code_{i}" name="type_code_{i}" value="{typ}" required>
<input type="text" id="desc_{i}" name="desc_{i}" required>
<input type="color" id="color_{i}" name="color_{i}" value="{default_color}" required>
</div>'''.format(i=i, typ=html.escape(typ), default_color=DEFAULT_COLOR))
print('''
<div class="form-group">
<label for="debug">Zobrazit debug informace:</label>
<input type="checkbox" id="debug" name="debug"{checked}>
</div>
<div class="form-group">
<input type="submit" value="Vygenerovat scénář">
</div>
</form>
</div>
<div class="footer">
<p>© 2024 Martin Sukaný • <a href="mailto:martin@sukany.cz">martin@sukany.cz</a></p>
</div>
</body>
</html>'''.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("<p>Chyba: Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.</p>")
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('''<div class="output-container">
<h2>Výsledky zpracování</h2>
<p><a href="/tmp/{name}" download>Stáhnout scénář pro {title} ZDE</a></p>
<p><strong>Název akce:</strong> {title}</p>
<p><strong>Detail akce:</strong> {detail}</p>
<h3>Data ze souboru:</h3>
{table}
<p><a href="/tmp/{name}" download>Stáhnout scénář pro {title} ZDE</a></p>
</div>'''.format(
name=html.escape(safe_name),
title=html.escape(title or ""),
detail=html.escape(detail or ""),
table=data.to_html(index=False)))
else:
print("<p>Chyba: Soubor nebyl nalezen. Zkontrolujte vstupní data.</p>")
else:
print("<p>Chyba: Není vybrán žádný soubor nebo došlo k chybě ve formuláři.</p>")