diff --git a/Dockerfile b/Dockerfile index 9138879..86bcd72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,11 @@ COPY templates ./templates # Ensure CGI scripts are executable RUN find /var/www/htdocs/cgi-bin -type f -name "*.py" -exec chmod 0755 {} \; -# Writable tmp for the app +# Writable tmp + kompatibilita s /scripts/tmp (skrypt nic neupravujeme) RUN mkdir -p /var/www/htdocs/tmp \ - && chown -R www-data:www-data /var/www/htdocs/tmp /var/www/htdocs/templates \ - && chmod 0775 /var/www/htdocs/tmp + /var/www/htdocs/scripts/tmp \ + && chown -R www-data:www-data /var/www/htdocs/tmp /var/www/htdocs/scripts \ + && chmod 0775 /var/www/htdocs/tmp /var/www/htdocs/scripts/tmp # --- Python dependencies (add more as needed) --- RUN pip install --no-cache-dir pandas openpyxl diff --git a/cgi-bin/scenar.py b/cgi-bin/scenar.py index 3ebb96b..58ffeaa 100755 --- a/cgi-bin/scenar.py +++ b/cgi-bin/scenar.py @@ -3,15 +3,22 @@ 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, time +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") @@ -24,9 +31,7 @@ detail = form.getvalue('detail') show_debug = form.getvalue('debug') == 'on' step = form.getvalue('step', '1') -file_item = None -if 'file' in form: - file_item = form['file'] +file_item = form['file'] if 'file' in form else None def get_program_types(form): program_descriptions = {} @@ -36,9 +41,10 @@ def get_program_types(form): index = key.split('_')[-1] type_code = form.getvalue(f'type_code_{index}') description = form.getvalue(f'desc_{index}', '') - color = 'FF' + form.getvalue(f'color_{index}', 'FFFFFF').lstrip('#') + 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 + program_colors[type_code] = color_hex return program_descriptions, program_colors program_descriptions, program_colors = get_program_types(form) @@ -52,12 +58,11 @@ def normalize_time(time_str): return None def read_excel(file_content): - excel_data = pd.read_excel(BytesIO(file_content), skiprows=0) # Do not skip the header row + excel_data = pd.read_excel(BytesIO(file_content), skiprows=0) excel_data.columns = ["Datum", "Zacatek", "Konec", "Program", "Typ", "Garant", "Poznamka"] if show_debug: - print("
")
-        print("Raw data:")
+        print("
Raw data:\n")
         print(excel_data.head())
         print("
") @@ -76,7 +81,7 @@ def read_excel(file_content): "index": index, "Datum": datum, "Zacatek": zacatek, - "Konec": row["Konec"], # Changed from `konec` to `row["Konec"]` to avoid ValueError in future calculations + "Konec": konec, "Program": row["Program"], "Typ": row["Typ"], "Garant": row["Garant"], @@ -84,55 +89,48 @@ def read_excel(file_content): "row_data": row }) except Exception as e: - error_rows.append({ - "index": index, - "row": row, - "error": str(e) - }) + error_rows.append({"index": index, "row": row, "error": str(e)}) valid_data = pd.DataFrame(valid_data) if show_debug: - print("
")
-        print("Cleaned data:")
+        print("
Cleaned data:\n")
         print(valid_data.head())
         print("
") - print("
")
-        print("Error rows:")
-        for error_row in error_rows:
-            print(f"Index: {error_row['index']}, Error: {error_row['error']}")
-            print(error_row['row'])
+        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 idx, row in sorted_group.iterrows(): - if previous_end_time and row['Zacatek'] < previous_end_time: + for _, r in sorted_group.iterrows(): + if previous_end_time and r['Zacatek'] < previous_end_time: overlap_errors.append({ - "index": row["index"], - "Datum": row["Datum"], - "Zacatek": row["Zacatek"], - "Konec": row["Konec"], - "Program": row["Program"], - "Typ": row["Typ"], - "Garant": row["Garant"], - "Poznamka": row["Poznamka"], + "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": row["row_data"] + "row_data": r["row_data"] }) - previous_end_time = row['Konec'] + previous_end_time = r['Konec'] if overlap_errors: if show_debug: - print("
")
-            print("Overlap errors:")
-            for error in overlap_errors:
-                print(error)
+            print("
Overlap errors:\n")
+            for e in overlap_errors:
+                print(e)
             print("
") - valid_data = valid_data[~valid_data.index.isin([error['index'] for error in overlap_errors])] + 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 @@ -140,15 +138,17 @@ def read_excel(file_content): def calculate_row_height(cell_value, column_width): if not cell_value: return 15 - max_line_length = column_width * 1.2 - lines = cell_value.split('\n') + 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.

") @@ -177,6 +177,10 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): 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 @@ -190,10 +194,9 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): 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("
")
-            print("Start times:")
+            print("
Start times:\n")
             print(start_times)
-            print("End times:")
+            print("\nEnd times:\n")
             print(end_times)
             print("
") return None @@ -202,17 +205,20 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): min_time = min(start_times) max_time = max(end_times) except ValueError as e: - print("

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

".format(e)) + print(f"

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

") if show_debug: - print("
")
-            print("Start times:")
+            print("
Start times:\n")
             print(start_times)
-            print("End 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 + 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) @@ -253,12 +259,9 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): 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("

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

".format(e)) + print(f"

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

") if show_debug: - print("
")
-                    print("Start time: {}".format(start_time))
-                    print("End time: {}".format(end_time))
-                    print("
") + print("
Start time: {}\nEnd time: {}
".format(start_time, end_time)) continue cell_value = f"{row['Program']}" @@ -274,14 +277,12 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): 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("
")
-                    print(f"Overlapping block:\n{row}")
-                    print("
") + print("
Overlapping block:\n{}
".format(row)) return None cell.alignment = Alignment(wrap_text=True, horizontal="center", vertical="center") - lines = cell_value.split("\n") - for idx, line in enumerate(lines): + lines = str(cell_value).split("\n") + for idx, _ in enumerate(lines): if idx == 0: cell.font = Font(bold=True) elif idx == 1: @@ -289,7 +290,9 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): 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.fill = PatternFill(start_color=program_colors[row["Typ"]], + end_color=program_colors[row["Typ"]], + fill_type="solid") cell.border = thick_border current_row += 1 @@ -305,8 +308,7 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): legend_max_length = max(legend_max_length, calculate_column_width(legend_text)) legend_row += 1 - ws.column_dimensions[get_column_letter(col_offset)].width = legend_max_length - + 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 @@ -326,119 +328,60 @@ def create_timetable(data, title, detail, program_descriptions, program_colors): return wb -def calculate_column_width(text): - max_length = max(len(line) for line in text.split('\n')) - return max_length * 1.2 - +# ====== HTML flow ====== if step == '1': - print(''' - - - - - Vytvoření Scénáře - Krok 1 - - - -
-

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

-

Vyplňte níže uvedený formulář k vytvoření nového scénáře. Prosím, vyplňte všechny povinné položky a přiložte požadovaný Excel soubor. Po vyplnění formuláře klikněte na tlačítko "Načíst typy programu".

-

STAHNOUT SABLONU SCENARE ZDE

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - - - - ''') + 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() @@ -448,165 +391,98 @@ elif step == '2' and file_item is not None and file_item.filename: 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] # Remove any whitespace characters + program_types = [typ.strip() for typ in program_types] - print(''' - - - - - Vytvoření Scénáře - Krok 2 - - - -
-

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

-

Prosím, vyplňte tituly a barvy pro nalezené typy programu. Po vyplnění formuláře klikněte na tlačítko "Vygenerovat scénář".

-
- - - - - '''.format(title, detail, file_content_base64)) + 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, i, i, typ, i, i, i, i)) +
+ + + + +
'''.format(i=i, typ=html.escape(typ), default_color=DEFAULT_COLOR)) print(''' -
- - -
-
- -
- -
- - - - '''.format(' checked' if show_debug else '')) +
+ + +
+
+ +
+ +
+ + +'''.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" - file_path = os.path.join("/var/www/htdocs/scripts/tmp", filename) + 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''' -
-

Výsledky zpracování

-

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

-

Název akce: {title}

-

Detail akce: {detail}

-

Data ze souboru:

- {data.to_html(index=False)} -

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

-
- ''') - - if error_rows: - print(f''' -
-

Ignorované řádky:

- - - - - - - ''') - for error_row in error_rows: - print(f''' - - - - - - ''') - print(''' -
IndexŘádekDůvod
{error_row["index"]}{error_row["row"].to_dict()}{error_row["error"]}
-
- ''') + 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: diff --git a/tmp/Zazitkovy kurz trabant.xlsx b/tmp/Zazitkovy kurz trabant.xlsx index 0f2e009..a9505e7 100644 Binary files a/tmp/Zazitkovy kurz trabant.xlsx and b/tmp/Zazitkovy kurz trabant.xlsx differ