copilot test

This commit is contained in:
Martin Sukany
2025-11-13 11:37:28 +01:00
parent 7ef8e20564
commit 9a7ffdeb2c
12 changed files with 356 additions and 2 deletions

14
.githooks/pre-commit Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# Pre-commit hook: run tests and prevent commit on failures.
# By default run fast tests (exclude integration). To include integration tests set RUN_INTEGRATION=1.
echo "Running pytest (fast) before commit..."
if [ "${RUN_INTEGRATION:-0}" = "1" ]; then
echo "RUN_INTEGRATION=1: including integration tests"
pytest -q
else
pytest -q -m "not integration"
fi
echo "Tests passed. Proceeding with commit."

79
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,79 @@
## Účel
Krátké, konkrétní instrukce pro AI agenta pracujícího v tomto repozitáři. Cílem je rychle pochopit architekturu, běžné konvence a kde dělat bezpečné úpravy.
## Velký obraz (architektura)
- Jedna hlavní aplikace: `cgi-bin/scenar.py` — jednosouborová CGI aplikace, která vykresluje HTML formuláře a generuje výsledný Excel soubor.
- Statické soubory a šablona: `templates/` (obsahuje `scenar_template.xlsx`).
- Webroot a temp: `DOCROOT` a `TMP_DIR` jsou v `cgi-bin/scenar.py` (výchozí `/var/www/htdocs` a `/var/www/htdocs/tmp`). Dockerfile také nastavuje DocumentRoot na `/var/www/htdocs` a vytváří `/var/www/htdocs/tmp`.
## Důležité soubory
- `cgi-bin/scenar.py` — hlavní logika (upload, parsování Excelu pomocí pandas, tvorba Excelu pomocí openpyxl, HTML formuláře). Hledejte: funkce `read_excel`, `create_timetable`, `get_program_types`, konstanty `DOCROOT`, `TMP_DIR`.
- `templates/scenar_template.xlsx` — excelová šablona, kterou UI nabízí ke stažení.
- `Dockerfile` — ukazuje produkční runtime (Python 3.12-slim, instalace `pandas` a `openpyxl`, Apache s CGI, DocumentRoot `/var/www/htdocs`, port 8080).
## Datový a HTTP tok (konkrétně z kódu)
- Kroková sekvence přes POST parameter `step` (1 → 2 → 3):
- `step=1`: zobrazí formulář s nahráním Excelu (pole `file`).
- `step=2`: načte Excel (pandas), zjistí unikátní typy (`Typ`) a zobrazí formulář pro zadání `type_code_{i}`, `desc_{i}`, `color_{i}`.
- `step=3`: vytvoří výsledný sešit, uloží ho do `TMP_DIR` a odkaz vrátí jako `/tmp/<safe_name>`.
## Pole formuláře a pojmenování
- Upload: pole `file` (v `step=1`).
- Základní metadatová pole: `title`, `detail`, `debug` (checkbox dává ladicí výstup), `step`.
- Dynamické pole pro typy: `type_code_{i}`, `desc_{i}`, `color_{i}` — exportované z dat a pak poskytnuté v `step=2`.
- Interně pro krok 3 se používá `file_content_base64` k bezpečnému přenosu obsahu mezi kroky.
## Konvence projektu
- HTML šablony nejsou separátní (JS/templating) — jsou inline v `scenar.py`. Upravujte opatrně: změny stylu/inl. HTML jsou v blocích `if step == '1'` / `step == '2'`.
- Soubor s výsledkem se ukládá s „safe“ jménem: povolena jsou alfanumerická znaky, `. _ -` a mezera. Pokud měníte pojmenování, ověřte odkazy `/tmp/<name>`.
- Barvy pro OpenPyXL: kód se převádí na formát AARRGGBB (`'FF' + raw_color.lstrip('#')`).
## Chyby, logika validací a okrajové případy
- Parsování času: funkce `normalize_time` podporuje formáty `%H:%M` a `%H:%M:%S`.
- `read_excel` validuje datum/časy a sbírá `error_rows`. Překryvy časů detekuje a tyto řádky vyřadí (viz `overlap_errors`).
- Pokud chybí definice typu (Typ v Excelu není přiřazen v UI), skript formou HTML vrací chybu.
## Závislosti a runtime
- Python: Dockerfile používá `python:3.12-slim` (projekt tedy cílí na moderní 3.12). Lokálně bude fungovat i Python 3.10+ (exports v repozitáři naznačují 3.10/3.9 přítomnost), ale pro přesnost použijte 3.12.
- Klíčové Python balíčky: `pandas`, `openpyxl`. Jsou instalovány v `Dockerfile`.
## Vývojové workflow (rychlé ověření lokálně)
- Rychlý způsob pro lokální testování bez Dockeru (repo má `cgi-bin/`):
1) Spusť z kořene repozitáře Python simple CGI server (vytvoří `cgi-bin/` endpointy):
python -m http.server --cgi 8000
2) Otevři v prohlížeči: http://localhost:8000/cgi-bin/scenar.py
- Pro produkční podobu použij `Dockerfile` (Apache + CGI) — Dockerfile vystavuje port 8080.
## Co upravovat a kde (rychlé reference)
- Změny chování Excelu / parsování: uprav `read_excel` a `normalize_time` v `cgi-bin/scenar.py`.
- Změny vzhledu formulářů: uprav HTML stringy v `if step == '1'` a `if step == '2'`.
- Cesty a přístupová práva: pokud měníš DocumentRoot/TMP, uprav i `DOCROOT`/`TMP_DIR` v `scenar.py` a odpovídající část v `Dockerfile`.
## Příklady (konkrétní ukázky z kódu)
- Detekce typů: `for key in form.keys(): if key.startswith('type_code_'):` — při rozšiřování formulářů zachovat tuto konvenci.
- Barva pro openpyxl: `color_hex = 'FF' + raw_color.lstrip('#')`.
- Safe filename: `safe_name = "".join(ch if ch.isalnum() or ch in "._- " else "_" for ch in filename)`.
## Poznámky pro agenta
- Buď konkrétní: ukaž přesné linie/názvy funkcí, pokud navrhuješ změnu. Např. "Uprav `calculate_row_height` v `create_timetable` když měníš rozměry řádků".
- Nevkládej náhodné závislosti — kontroluj `import` v `cgi-bin/scenar.py` a `Dockerfile`.
- Pokud budeš navrhovat CLI nebo refaktor na framework (Flask apod.), nejprve vyznač migraci: oddělit HTML do šablon a nasadit routy místo CGI.
---
Pokud chceš, upravím instrukci na míru (více/ méně detailů) nebo přidám krátký seznam rychlých úkolů (např. přidat `requirements.txt`, unit testy pro `read_excel`).

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Runtime / local env
.venv/
tmp/
.pytest_cache/
__pycache__/
.DS_Store
# test artifacts
tests/tmp/

View File

@@ -1,3 +1,70 @@
# Scenar-creator
# Scenar Creator
Repozitar obsahuje nastroj pro vytvareni scenaru pro zazitkove kurzy
Jednoduchá CGI aplikace pro vytváření časových plánů (Excel) z nahraného Excelu se seznamem programů.
Krátký, praktický popis, jak projekt spustit, testovat a kde hledat důležité části kódu.
## Co to dělá
- `cgi-bin/scenar.py` načte Excel s řádky obsahujícími Datum, Začátek, Konec, Program, Typ, Garant, Poznámka.
- Na základě polí `Typ` vytvoří HTML formulář pro přiřazení popisů a barev, a nakonec vygeneruje výsledný sešit (OpenPyXL) s časovou osou.
## Rychlý start (lokálně, bez Dockeru)
1) V kořeni repozitáře spusť jednoduchý CGI server (vyžaduje Python):
```bash
python -m http.server --cgi 8000
# Otevři v prohlížeči: http://localhost:8000/cgi-bin/scenar.py
```
2) Alternativně připrav virtuální prostředí a spusť testy:
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytest -q
```
## Docker (produkční přiblížení)
Dockerfile vytváří image na `python:3.12-slim`, instaluje `pandas` a `openpyxl`, nastaví Apache s povoleným CGI a DocumentRoot na `/var/www/htdocs`. Nasazení:
```bash
docker build -t scenar-creator .
docker run -p 8080:8080 scenar-creator
# Pak otevři: http://localhost:8080/
```
## Testy
- Testy jsou v `tests/` a používají `pytest`.
- Přidal jsem testy pro funkci `read_excel` (happy path + invalid time). Spuštění viz výše.
## Struktura projektu (klíčové soubory)
- `cgi-bin/scenar.py` — hlavní CGI skript (HTML generováno inline). Hledej funkce:
- `read_excel(file_content)` — parsování vstupního xlsx (pandas) → vrací (valid_data, error_rows)
- `create_timetable(...)` — vytváří OpenPyXL sešit
- `get_program_types(form)` — načítání dynamických polí `type_code_{i}`, `desc_{i}`, `color_{i}`
- `templates/scenar_template.xlsx` — šablona pro uživatele
- `tmp/` — místo, kam se výsledné soubory ukládají při běhu (v Dockeru `/var/www/htdocs/tmp`)
- `.github/copilot-instructions.md` — instrukce pro AI agenty (přehled konvencí a místa úprav)
## Konvence a poznámky pro vývojáře
- HTML jsou inline stringy v `scenar.py` (sekce `if step == '1'`, `if step == '2'`, `if step == '3'`). Pokud budeš měnit UI, uprav tyto bloky.
- Výstupní soubor se ukládá s "safe" jménem: povolena písmena, čísla a `. _ -` a mezera.
- Barvy pro OpenPyXL: v kódu se převádějí na AARRGGBB pomocí `'FF' + raw_color.lstrip('#')`.
- Parsování času: `normalize_time` podporuje `%H:%M` a `%H:%M:%S`.
## Doporučené další kroky
- Přidat `requirements.txt` (hotovo) s pevnými verzemi pro deterministické buildy.
- Přesunout jádro logiky z `cgi-bin/scenar.py` do importovatelného modulu (např. `scenar_core.py`) pro snazší testování a údržbu.
- Přidat GitHub Actions workflow, který spouští `pytest` na PR.
## Kontakt
Autor: Martin Sukaný — martin@sukany.cz

View File

@@ -92,6 +92,11 @@ def read_excel(file_content):
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")

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
markers =
integration: marks tests as integration (docker builds / long-running)

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pandas
openpyxl
pytest

14
scripts/install_hooks.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -eu
# Install repository-local git hooks by setting core.hooksPath
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOKS_DIR="$REPO_ROOT/.githooks"
if [ ! -d "$HOOKS_DIR" ]; then
echo "No .githooks directory found in repo root: $HOOKS_DIR"
exit 1
fi
git config core.hooksPath "$HOOKS_DIR"
echo "Installed git hooks path: $HOOKS_DIR"
echo "You can revert with: git config --unset core.hooksPath"

10
scripts/start_scenar.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -eu
# Usage: ./scripts/start_scenar.sh [image] [container_name] [port]
IMAGE=${1:-scenar-creator:latest}
NAME=${2:-scenar-creator}
PORT=${3:-8080}
echo "Starting container '$NAME' from image '$IMAGE' on port $PORT..."
docker run -d --name "$NAME" -p "$PORT:8080" "$IMAGE"
echo "Container started."

8
scripts/stop_scenar.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu
# Usage: ./scripts/stop_scenar.sh [container_name]
NAME=${1:-scenar-creator}
echo "Stopping and removing container '$NAME'..."
docker rm -f "$NAME" >/dev/null 2>&1 || true
echo "Container removed (if it existed)."

View File

@@ -0,0 +1,56 @@
import os
import shutil
import subprocess
import time
import socket
import urllib.request
import pytest
def docker_available():
return shutil.which("docker") is not None
def wait_for_http(url, timeout=30):
end = time.time() + timeout
last_exc = None
while time.time() < end:
try:
with urllib.request.urlopen(url, timeout=3) as r:
body = r.read().decode('utf-8', errors='ignore')
return r.getcode(), body
except Exception as e:
last_exc = e
time.sleep(0.5)
raise RuntimeError(f"HTTP check failed after {timeout}s: {last_exc}")
@pytest.mark.skipif(not docker_available(), reason="Docker is not available on this runner")
@pytest.mark.integration
def test_build_run_and_cleanup_docker():
image_tag = "scenar-creator:latest"
container_name = "scenar-creator-test"
port = int(os.environ.get('SCENAR_TEST_PORT', '8080'))
# Build image
subprocess.run(["docker", "build", "-t", image_tag, "."], check=True)
# Ensure no leftover container
subprocess.run(["docker", "rm", "-f", container_name], check=False)
try:
# Run container
subprocess.run([
"docker", "run", "-d", "--name", container_name, "-p", f"{port}:8080", image_tag
], check=True)
# Wait for HTTP and verify content
code, body = wait_for_http(f"http://127.0.0.1:{port}/")
assert code == 200
assert "Vytvoření Scénáře" in body or "Scenar" in body or "Vytvoření" in body
finally:
# Cleanup container and image
subprocess.run(["docker", "rm", "-f", container_name], check=False)
subprocess.run(["docker", "rmi", image_tag], check=False)

86
tests/test_read_excel.py Normal file
View File

@@ -0,0 +1,86 @@
import io
import os
import pandas as pd
import importlib.util
import sys
import types
def load_scenar_module():
repo_root = os.path.dirname(os.path.dirname(__file__))
scenar_path = os.path.join(repo_root, 'cgi-bin', 'scenar.py')
# Provide a minimal fake `cgi` module so top-level imports in the CGI script don't fail
if 'cgi' not in sys.modules:
fake_cgi = types.ModuleType('cgi')
class FakeFieldStorage:
def getvalue(self, key, default=None):
return default
def keys(self):
return []
def __contains__(self, item):
return False
fake_cgi.FieldStorage = FakeFieldStorage
sys.modules['cgi'] = fake_cgi
# minimal fake cgitb (some environments don't expose it)
if 'cgitb' not in sys.modules:
fake_cgitb = types.ModuleType('cgitb')
def fake_enable():
return None
fake_cgitb.enable = fake_enable
sys.modules['cgitb'] = fake_cgitb
spec = importlib.util.spec_from_file_location('scenar', scenar_path)
module = importlib.util.module_from_spec(spec)
# executing the module will run top-level CGI code (prints etc.) but defines functions we need
spec.loader.exec_module(module)
return module
def make_excel_bytes(df: pd.DataFrame) -> bytes:
bio = io.BytesIO()
with pd.ExcelWriter(bio, engine='openpyxl') as writer:
df.to_excel(writer, index=False)
return bio.getvalue()
def test_read_excel_happy_path():
scenar = load_scenar_module()
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': ['09:00'],
'Konec': ['10:00'],
'Program': ['Test program'],
'Typ': ['WORKSHOP'],
'Garant': ['Garant Name'],
'Poznamka': ['Pozn']
})
content = make_excel_bytes(df)
valid, errors = scenar.read_excel(content)
assert isinstance(valid, pd.DataFrame)
assert len(errors) == 0
assert len(valid) == 1
assert valid.iloc[0]['Program'] == 'Test program'
def test_read_excel_invalid_time():
scenar = load_scenar_module()
df = pd.DataFrame({
'Datum': [pd.Timestamp('2025-11-13').date()],
'Zacatek': ['not-a-time'],
'Konec': ['10:00'],
'Program': ['Bad Time'],
'Typ': ['LECTURE'],
'Garant': [None],
'Poznamka': [None]
})
content = make_excel_bytes(df)
valid, errors = scenar.read_excel(content)
# invalid time should produce at least one error row and valid may be empty
assert isinstance(errors, list)
assert len(errors) >= 1