diff --git a/.env.example b/.env.example index be46e3f..8419f30 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,7 @@ PORT=8000 # Host directory bind-mounted at /app/data (used by docker compose only). DATA_DIR=./data -# Initial admin is created only if no users exist. +# Initial admin is created only if no teams exist (normally the first start). INITIAL_ADMIN_NAME=Administrador INITIAL_ADMIN_USERNAME=admin INITIAL_ADMIN_EMAIL=admin@example.com diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aa7542f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# Repository guide + +## Project and layout + +Contínuos CRR is a server-rendered shift rota application. It uses Python +(the Docker runtime is 3.12), FastAPI, synchronous SQLAlchemy, SQLite, Jinja2, +and Alembic. Dependencies live in `requirements.txt`; development dependencies +live in `requirements-dev.txt`. + +- `app/main.py`: FastAPI application, lifespan, middleware, templates, static files. +- `app/web.py`: routes, session authentication, CSRF, administration, swap workflows. +- `app/models.py`: SQLAlchemy schema and relationships. +- `app/database.py`: engine, sessions, migrations, SQLite backups. +- `app/config.py`: Pydantic settings, environment/`.env` loading, proxy-path helpers. +- `app/scheduling.py`: monthly pattern and rotation generation. +- `app/bootstrap.py`: initial administrator and seeded schedules. +- `app/calendar.py`: iCalendar feeds; `app/i18n.py`: Portuguese labels and formatting. +- `app/notifications.py`: synchronous SMTP delivery and notification history. +- `app/security.py`: password hashing and CSRF token generation. +- `app/cli.py`: database initialization/migration, demo seeding, shell, reminders. +- `app/templates/`, `app/static/`: Jinja HTML and CSS/assets; no frontend build step. +- `migrations/`: Alembic environment and the squashed `0001_initial` revision. +- `alembic.ini`: migration paths and logging; the database URL comes from settings. +- `compose.yaml`: example production deployment with persistent SQLite storage. +- `tests/`: pytest discovery directory; currently empty. + +Read `docs/DOMAIN.md` for business rules and `docs/ARCHITECTURE.md` for lifecycle +and deployment details. See `README.md` for the test workflow. + +## Development conventions + +- Follow existing typed Python, snake_case names, and SQLAlchemy 2 mapped models. + Keep database and request handling synchronous, consistent with the application. +- Keep scheduling calculations in `app/scheduling.py`, persistence in models/database, + and HTTP orchestration in `app/web.py`. Reuse existing helpers. +- Keep user-facing text in Portuguese and reuse `app/i18n.py` and template filters. +- Build internal links with `settings.url()` (`app_path` in templates) and use + `_redirect()` in routes so deployments under `ROOT_PATH` keep working. +- Preserve session authorization and CSRF checks when adding form actions. +- Change the model and add an Alembic revision together for schema changes. + `app/database.py` builds the migration configuration using the root `alembic.ini` + and the database URL from settings. + +## Domain invariants + +- Users are logins; teams own shifts. One user may control several teams. +- Night assignments follow a repeating day-of-month pattern. Saturday and Sunday + lunch rotations are independent and continue from the previous actual assignee. +- Ordinary generation fills missing assignments only. Existing assignments change + through explicit administration or swap actions; preserve manual/swapped rows + when an operation calls for changing only generated rows. +- Swaps exchange two shifts in the same schedule atomically. Check current ownership + and cancel conflicting open requests when assignments change. +- Commit scheduling changes before notifications. Email failures must not undo them. + +## Testing + +Run from the repository root: + +```sh +make test +``` + +This creates/reuses `.venv`, installs development dependencies, and runs pytest. +No activation, application server, Docker service, or separate setup command is +needed. Python 3.12+ with `venv`/pip and `make` are prerequisites. + +- Use pytest functions and plain assertions in `tests/test_*.py`. +- Use FastAPI's `TestClient` (backed by HTTPX) for HTTP tests. +- Add shared fixtures to `tests/conftest.py` when needed. Fixtures must manage + temporary databases, initialization, dependency overrides, and cleanup so + `make test` remains the only command required. +- Settings and the database engine are created at import time. Set test environment + values before importing app modules; use temporary SQLite data, disable real SMTP, + and keep tests independent of local `.env` values and persistent application data. +- Prefer behavior and domain invariants over assertions about implementation details. +- An empty suite succeeds through `make test`; test failures and pytest errors still + fail the command. No tests or application fixtures have been added yet. + +Run `make test` after relevant changes and report its result accurately. An empty +suite verifies the runner only, not application behavior. diff --git a/Dockerfile b/Dockerfile index 80b40c5..920a50a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,7 @@ RUN mkdir -p /app/data EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"] -# Development image: base plus dev dependencies. Used by docker-compose.dev.yml, -# which supplies the reload command and per-service environment. +# Development image: base plus dev dependencies and auto-reload. FROM base AS dev COPY requirements-dev.txt ./ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2027968 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: test + +test: + @test -x .venv/bin/python || python3 -m venv .venv + @.venv/bin/python -m pip install --quiet -r requirements-dev.txt + @.venv/bin/python -m pytest $(ARGS); status=$$?; \ + if [ $$status -eq 5 ]; then exit 0; else exit $$status; fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..b63c49e --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Contínuos CRR + +A shift rota application built with FastAPI, SQLite/SQLAlchemy, and server-rendered +Jinja templates. It manages night and lunch schedules, teams, shared user logins, +shift swaps, calendar subscriptions, and optional email notifications. + +## Run with Docker Compose + +`compose.yaml` is a minimal deployment example. With Docker and Docker Compose +installed, copy the example settings and edit `.env`: + +```sh +cp .env.example .env +``` + +Set `SECRET_KEY` to a long random value and choose `INITIAL_ADMIN_PASSWORD` before +the first start. Set `BASE_URL` to the address users will access; SMTP is optional. + +Build and start the application: + +```sh +docker compose up --build -d +``` + +Open and sign in with `INITIAL_ADMIN_USERNAME` and +`INITIAL_ADMIN_PASSWORD`. Startup applies database migrations and creates the initial +administrator and schedules automatically. Initial administrator settings only apply +when the database is first seeded. + +- `PORT` selects the host port (default `8000`); update `BASE_URL` if you change it. +- `DATA_DIR` selects the host data directory (default `./data`). The SQLite database + and default startup backups persist there across container rebuilds and restarts. +- Compose sets `DATABASE_URL` to `sqlite:////app/data/bar_rota.db` so the database + stays inside the persistent mount. If setting `BACKUP_DIR`, use a container path + beneath `/app/data` to persist backups too. +- For a reverse proxy, set `BASE_URL` to the public URL, `ROOT_PATH` if served under + a sub-path, and `SESSION_HTTPS_ONLY=true` when served over HTTPS. + +View logs or stop the service: + +```sh +docker compose logs -f app +docker compose down +``` + +Stopping the service leaves the host data directory intact. + +## Run tests + +From the repository root, run one command: + +```sh +make test +``` + +You need Python 3.12+ with `venv`/pip and `make`. The command automatically creates +`.venv` if needed, installs `requirements-dev.txt` (including application +dependencies), and runs pytest. Dependency installation needs package-index access +on the first run. You do not need to activate a virtualenv, start the application, +or run Docker or database setup commands. + +The framework is **pytest**, with **HTTPX** available for FastAPI's `TestClient`. +Configuration is in `pyproject.toml`; tests are discovered under `tests/`. +There are no tests yet: pytest will report `no tests ran`, and `make test` treats +that result as success. Actual failures and pytest errors return a nonzero status. + +Optional pytest arguments use the same command: + +```sh +make test ARGS="-k rotation -v" +``` + +## Adding tests later + +- Put files in `tests/test_*.py`, name functions `test_*`, and use plain `assert`. +- Use pytest's `tmp_path` and `monkeypatch` for isolated data and settings. +- Add database and HTTP client fixtures to `tests/conftest.py` as they become needed. + Fixtures should initialize a fresh temporary SQLite database and clean up FastAPI + dependency overrides automatically. Do not require manual migrations or seeding. +- Set environment variables before importing app modules: settings read `.env`, + and the SQLAlchemy engine is created at import time. Tests must use temporary + databases and disable real SMTP rather than inherit local application settings. +- HTTP tests use `fastapi.testclient.TestClient`. Its context manager runs the app's + lifespan, including backups, migrations, and initial data; configure those against + temporary storage before entering it. +- Test observable behavior: scheduling boundaries, preservation of existing shifts, + permissions/CSRF, swap ownership and transitions, calendar output, and email failure + handling. Use fixed dates and explicit data so tests do not depend on execution order. + +## Repository documentation + +- [Agent/contributor guide](AGENTS.md) +- [Architecture and lifecycle](docs/ARCHITECTURE.md) +- [Domain rules](docs/DOMAIN.md) diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..e7c98f8 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,37 @@ +[alembic] +script_location = %(here)s/migrations +prepend_sys_path = . + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..02a1cf0 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +services: + app: + build: + context: . + target: production + restart: unless-stopped + env_file: + - .env + environment: + # Keep the container's SQLite database on the persistent data mount. + DATABASE_URL: sqlite:////app/data/bar_rota.db + ports: + - "${PORT:-8000}:8000" + volumes: + - "${DATA_DIR:-./data}:/app/data" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88176a8..37a15d1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,8 +24,14 @@ Single FastAPI process with server-rendered HTML and a SQLite database volume. T ## Deployment -`docker-compose.yml` mounts `./data` at `/app/data`, so the SQLite DB persists across image rebuilds. It is the service stack only. `docker-compose.dev.yml` is the development stack: it builds the `dev` image target, bind-mounts `app/` and `tests/` for auto-reload, and hosts the containerized test runner. +`Dockerfile` provides a `production` target (the default) and a `dev` target with development dependencies and Uvicorn auto-reload. Both serve `app.main:app` on port 8000. `compose.yaml` is a minimal deployment example: it builds the production target, loads `.env`, publishes `${PORT:-8000}`, and mounts `${DATA_DIR:-./data}` at `/app/data`. It fixes the container database URL to the SQLite file on that mount; default backups also live there. See [the deployment guide](../README.md#run-with-docker-compose) for setup commands. -The deployment stack builds the `production` target (also the default Docker image target), which inherits the non-reloading runtime from `base`. Development also mounts Alembic configuration and migrations so schema changes can be authored in a container. `make` database commands use the host virtualenv by default, or a one-off development container with `RUNTIME=docker`. Both use the same `data/` directory by default. The checks service has independent settings and no persistent data mount, allowing app data entry and automated tests to run concurrently. +Maintenance commands are provided by `python -m app.cli`: `init-db`, `migrate`, `seed-demo`, `db-shell`, and `send-reminders`. Settings come from environment variables and `.env` through `app/config.py`. + +`alembic.ini` supplies migration paths and logging configuration. The actual database URL comes from application settings, configured programmatically in `app/database.py` and reused by `migrations/env.py`. For larger installations, the SQLAlchemy configuration can point to PostgreSQL, but the project intentionally ships only the SQLite deployment path today. + +## Testing + +Run `make test` on the host. It creates/reuses `.venv`, installs development dependencies, and invokes pytest; it needs no running application or container. `pyproject.toml` limits discovery to `tests/`, and HTTPX supports future FastAPI `TestClient` tests. The suite is currently empty. See [the testing guide](../README.md#run-tests) for conventions and automatic database-isolation requirements for future fixtures. diff --git a/pyproject.toml b/pyproject.toml index 6f52e08..9d5f155 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ [tool.pytest.ini_options] pythonpath = ["."] -testpaths = [] +testpaths = ["tests"] addopts = "-ra" diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29