copilot test
This commit is contained in:
14
.githooks/pre-commit
Executable file
14
.githooks/pre-commit
Executable 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
79
.github/copilot-instructions.md
vendored
Normal 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
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Runtime / local env
|
||||
.venv/
|
||||
tmp/
|
||||
.pytest_cache/
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
|
||||
# test artifacts
|
||||
tests/tmp/
|
||||
71
README.md
71
README.md
@@ -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
|
||||
|
||||
@@ -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
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
markers =
|
||||
integration: marks tests as integration (docker builds / long-running)
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
pandas
|
||||
openpyxl
|
||||
pytest
|
||||
14
scripts/install_hooks.sh
Executable file
14
scripts/install_hooks.sh
Executable 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
10
scripts/start_scenar.sh
Normal 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
8
scripts/stop_scenar.sh
Normal 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)."
|
||||
56
tests/test_docker_integration.py
Normal file
56
tests/test_docker_integration.py
Normal 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
86
tests/test_read_excel.py
Normal 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
|
||||
Reference in New Issue
Block a user