Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Agent Instructions Pointer

Tento soubor je záměrně stručný.

Veškerá pravidla a instrukce jsou v `AGENTS.md` v rootu repozitáře.

## Povinný postup

1. Nejdřív načti `AGENTS.md`.
2. Řiď se výhradně pravidly z `AGENTS.md`.
3. Pokud je zde cokoliv v konfliktu s `AGENTS.md`, přednost má `AGENTS.md`.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,5 @@ venv.bak/
# mypy
.mypy_cache/

# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
# cursor settings
.cursor/
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
162 changes: 162 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# AGENTS.md

Tento soubor poskytuje instrukce AI agentům při práci s kódem v tomto repozitáři.

## Příkazy

### Lokální spuštění (od nuly)

```bash
# 1. Zkopíruj a vyplň env proměnné (DATABASE_URL, SECRET_KEY, GPR_TOKEN, ...)
cp .env.template .env

# 2. Nainstaluj závislosti
pipenv install --dev # Python závislosti
npm ci # Node závislosti (postinstall hook automaticky nainstaluje i frontend a buildne ho)

# 3. (Pouze při prvním spuštění native režimu) vytvoř DB kontejner postgresql_cz
source scripts/shell/postgresql_docker.sh

# 4. Spusť databázi a servery
make db # PostgreSQL v Docker kontejneru
make be # Django dev server na 0.0.0.0:8000
make fe # Webpack dev server na http://localhost:3000
```

**Alternativa přes Docker Compose** (spustí vše včetně DB):
```bash
docker compose up
docker compose run web python manage.py createsuperuser
```

> **FontAwesome PRO:** ikony jsou z private GitHub Package Registry — `GPR_TOKEN` v `.env` musí být nastaven i pro lokální vývoj (jinak `npm ci` selže).

### Backend (Python / Django)

```bash
# Testy a kvalita kódu
pipenv run mypy . # typová kontrola
pipenv run python manage.py test # unit testy (Django TestCase)
pipenv run python manage.py behave --stage=api --format=progress3 # E2E API testy (behave)
pipenv run python manage.py behave --stage=ui --format=progress3 # E2E UI testy (behave + Selenium)

# Coverage (kombinuje všechny testy dohromady)
pipenv run coverage run -a manage.py test
pipenv run coverage run -a manage.py behave --stage=api --format=progress3
pipenv run coverage run -a manage.py behave --stage=ui --format=progress3
pipenv run coverage report

# Migrace
pipenv run python manage.py makemigrations # vytvoří nové migrace po změně modelů
pipenv run python manage.py migrate # aplikuje migrace na DB
```

> **PostgreSQL je povinné i pro testy:** před `pipenv run python manage.py test` nebo `behave` nejdřív spusť DB (`make db` v native režimu, nebo `docker compose up` v compose režimu). Bez běžící PostgreSQL backend testy selžou už při inicializaci testovací databáze.

### Frontend (TypeScript / React)

```bash
# Testy a kvalita kódu (z rootu repozitáře — doporučeno pro CI paritu)
npm run frontend:test # typy + lint + jest (kompletní frontend CI suite)
npm run frontend:lint:js # pouze ESLint
npm run frontend:audit # security audit závislostí (audit-ci)

# Detailní příkazy (ze složky frontend/)
cd frontend
npm run types # TypeScript typová kontrola (tsc)
npm run types:watch # tsc ve watch módu
npm run lint # ESLint + Prettier check
npm run lint! # ESLint + Prettier autofix
npm run jest # pouze Jest testy
npm run jest:watch # Jest ve watch módu
npm run build # produkční build (Webpack)
npm run build:analyze # bundle analyzer
```

## Architektura

Systém pro správu lekcí a kurzů — Django REST API backend + React SPA frontend, nasazení na Fly.io.

**Větev pro vývoj a CI je `master`** (ne `main`).

### Backend

Django 6 + Django REST Framework — REST API pro všechny operace. Kód je rozdělen do Django aplikací:

- [api/](api/) — DRF viewsets, serializéry, filtry, business logika; **zde žije veškerá API logika**
- [admin/](admin/) — modely, Django admin interface, šablony (shell pro SPA)
- [up/](up/) — projekt config: nastavení (`settings/base.py`, `settings/local.py`, `settings/production.py`), hlavní URL routing
- [tests/](tests/) — BDD E2E testy (behave), feature soubory v Gherkin, Selenium UI testy
- [scripts/shell/](scripts/shell/) — shell skripty pro CI (`ci/`) a lokální setup

**Modely** jsou v [admin/models.py](admin/models.py): klienti, skupiny, kurzy, lekce, přihlášky, stavy docházky.

**Nastavení:** `up/settings/` — `base.py` je základ, `local.py` a `production.py` ho přetěžují. V CI se používá `production.py` (`DJANGO_SETTINGS_MODULE=up.settings.production`). Lokálně se bere z `.env`.

**Python konvence:**
- Formátování: Black (`line-length = 100`)
- Typování: mypy — veškerý nový kód musí mít typové anotace, mypy nesmí hlásit chyby
- Dead code: vulture — nepoužívané symboly jsou chybou
- Závislosti: Pipenv (`Pipfile` + `Pipfile.lock`) — nikdy `pip install` přímo

### Frontend

React 19 SPA v [frontend/src/](frontend/src/). Webpack dev server na portu 3000 se napojuje na Django přes proxy (HMR).

**`npm ci` z rootu** nainstaluje root závislosti (Husky) a přes `postinstall` hook automaticky provede `cd frontend && npm ci && npm run build:ci`. Pro vývoj je proto potřeba `npm ci` spustit z rootu, ne ze složky `frontend/`.

**Klíčové knihovny:**
- Routing: TanStack Router (`frontend/src/router.tsx`, URL konstanty v `frontend/src/APP_URLS.ts`)
- Server state: TanStack Query (React Query) — veškerá komunikace s API
- CSS: vanilla-extract (type-safe CSS-in-JS, soubory `*.css.ts`)
- UI: Reactstrap (Bootstrap 5 wrappery) + FontAwesome PRO ikony
- Fuzzy search: Fuse.js
- Grafy: Recharts

**Struktura `frontend/src/`:**
- `api/` — API client (axios) a query hooky (TanStack Query)
- `components/` — sdílené React komponenty
- `pages/` — stránkové komponenty (Diary, Dashboard, atd.)
- `forms/` — formulářové komponenty
- `types/` — TypeScript typy sdílené napříč aplikací
- `hooks/` — custom React hooky
- `contexts/` — React Context provídery

**Frontend konvence:**
- Formátování: Prettier (`tabWidth: 4`, `printWidth: 100`)
- Linting: ESLint 9 s pluginy (react, typescript, jest, testing-library, vanilla-extract, tanstack-query)
- CSS: soubory pojmenovány `*.css.ts`, **vždy** vanilla-extract — nikdy inline styly ani plain CSS
- Testy: Jest + React Testing Library, soubory colocated se zdrojovým kódem (`*.test.ts` / `*.test.tsx`), API mockované přes MSW

**Pre-commit hooky (Husky + lint-staged):** automaticky spouštějí ESLint a Prettier na staged JS/TS souborech.

## Ověření změn

- **Po každé úpravě frontend souborů** (přidání importů, nové komponenty, přeformátování) spusť autofix před kontrolou:
```bash
cd frontend && npm run lint!
```
ESLint + Prettier opraví pořadí importů, zbytečné mezery a další formátovací chyby, které jinak hlásí CI jako errory.
- Čistě frontend změny ověř primárně přes `npm run frontend:test`.
- Backend změny ověř přes `pipenv run mypy .` a relevantní `manage.py test` / `behave`, ale až po spuštění PostgreSQL.
- Změny přesahující backend i frontend ideálně ověř na obou stranách; lokální "green" frontend testy neříkají nic o dostupnosti DB nebo API.

### Build a nasazení

**CI** ([`.github/workflows/test.yml`](.github/workflows/test.yml)) se spouští na každý push/PR do `master`:
1. Nainstaluje Node 20 + Python 3.12 + závislosti
2. Vytvoří `.npmrc` pro FontAwesome PRO z private GitHub Package Registry (token `GPR_TOKEN`)
3. Spustí frontend testy (typy + lint + jest)
4. Spustí mypy
5. Nastartuje PostgreSQL 14 s českou locale v Dockeru
6. Buildne Django (migrace + staticfiles) přes `scripts/shell/release_tasks.sh`
7. Django deployment checklist
8. Django unit testy + E2E API testy + E2E UI testy (behave + Selenium/Firefox)
9. Nahraje code coverage do Codecov
10. Nasadí testing verzi na Fly.io (přeskočí pro Dependabot)

**Deploy** ([`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)) se spouští na git tagy — nasadí produkci na Fly.io a pushne Docker image do ghcr.io.

**Prostředí:**
- Testing: automaticky nasazeno z `master`
- Produkce: manuálně přes git tag
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@AGENTS.md

# Agent Instructions Pointer

Tento soubor je záměrně stručný.

Veškerá pravidla a instrukce jsou v `AGENTS.md` v rootu repozitáře.

## Povinný postup

- Řiď se výhradně pravidly z `AGENTS.md`.
- Pokud je zde cokoliv v konfliktu s `AGENTS.md`, přednost má `AGENTS.md`.
52 changes: 26 additions & 26 deletions api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ class LectureFilter(filters.FilterSet):
Filtr lekcí podle startu (date), skupiny (group) a klienta (client).
Filtr skupiny je základní.
Filtr startu a klienta umožňuje filtrovat jednodušším URL parametrem, než konkrétní cestou k related_field (xx_yy).
Filtr klienta navíc odstraní z výsledku skupinové lekce.
Filtr klienta ve výchozím stavu vrátí jen individuální lekce (group__isnull=True).
Parametr includeGroup=true přidá i skupinové lekce klienta (group__isnull=False).
"""

date = filters.DateFilter(field_name="start__date")
client = filters.NumberFilter(field_name="attendances__client", method="filter_client")
includeGroup = filters.BooleanFilter(method="filter_include_group")

def filter_client(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
"""
Filtr podle klienta, kde name je aktuální filtrované pole (klient),
value je jeho hodnota (ID klienta).
"""
# aby bylo mozne rozsirit filtr na group__isnull, sami si praci s filtrem nad querysetem obstarame
return queryset.filter(**{name: value}, group__isnull=True)
# parametr includeGroup se zpracovava spolecne s filtrem client
include_group = self.form.cleaned_data.get("includeGroup")
q = queryset.filter(**{name: value})
if not include_group:
q = q.filter(group__isnull=True)
return q

def filter_include_group(self, queryset: QuerySet, name: str, value: bool) -> QuerySet:
# samostatny includeGroup (bez client) nema efekt; zpracovani probiha ve filter_client
return queryset

class Meta:
model = Lecture
Expand All @@ -40,28 +46,22 @@ class GroupFilter(filters.FilterSet):
Parametr onlyPast=true vrátí jen skupiny, které klient opustil (má tam účast na lekci, ale již není členem).
"""

client = filters.NumberFilter()
onlyPast = filters.BooleanFilter()
client = filters.NumberFilter(method="filter_client")
onlyPast = filters.BooleanFilter(method="filter_only_past")

def filter_queryset(self, queryset: QuerySet) -> QuerySet:
client_id = self.form.cleaned_data.get("client")
def filter_client(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
# parametr onlyPast se zpracovava spolecne s filtrem client
only_past = self.form.cleaned_data.get("onlyPast")
active = self.form.cleaned_data.get("active")

if client_id is not None:
if only_past:
queryset = (
queryset
.filter(lectures__attendances__client=client_id)
.exclude(memberships__client__pk=client_id)
.distinct()
)
else:
queryset = queryset.filter(memberships__client__pk=client_id)

if active is not None:
queryset = queryset.filter(active=active)
if only_past:
return (
queryset.filter(lectures__attendances__client=value)
.exclude(memberships__client__pk=value)
.distinct()
)
return queryset.filter(memberships__client__pk=value)

def filter_only_past(self, queryset: QuerySet, name: str, value: bool) -> QuerySet:
# samostatny onlyPast (bez client) nema efekt; filtrovani probiha ve filter_client
return queryset

class Meta:
Expand Down
3 changes: 3 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class ClientSerializer(serializers.ModelSerializer[Client]):
Serializer pro klienta lektorky.
"""

last_lecture_date = serializers.DateTimeField(read_only=True, allow_null=True, default=None)

class Meta:
model = Client
fields = "__all__"
Expand Down Expand Up @@ -147,6 +149,7 @@ class GroupSerializer(ValidateCourseIdMixin, serializers.ModelSerializer[Group])
Serializer skupiny klientů nějakého kurzu.
"""

last_lecture_date = serializers.DateTimeField(read_only=True, allow_null=True, default=None)
# nazev skupiny (znovuuvedeni kvuli validaci unikatnosti)
name = serializers.CharField(
validators=[UniqueValidator(queryset=Group.objects.all())],
Expand Down
2 changes: 2 additions & 0 deletions api/tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def ids(response_data: list[dict]) -> set[int]:
return {item["id"] for item in response_data}
Loading
Loading