# RAD Project — Agent Guidelines

Django-based radiology education platform. Multi-app, HTMX-driven, PostgreSQL + Redis + Celery stack.

## Architecture

### Apps
`generic` · `anatomy` · `physics` · `rapids` · `shorts` · `longs` · `sbas` · `wally` · `atlas` · `rcr` · `rota` · `oef`

- `generic/` — shared base models, views, forms, mixins, decorators, widgets. **Always check here before adding new base classes or utilities.**
- `atlas/` — CaseCollection-based learning (DICOM cases, findings, display sets, differentials)
- `rad/` — project config (`settings.py`, `urls.py`, `celery.py`, root API)

### Request/Response Pattern
- HTMX-first: `if request.htmx:` returns a partial, else returns a full page.
- Full pages extend `base.html`. HTMX partials live in `{app}/templates/{app}/partials/`.
- Partials must be standalone fragments — no `{% extends %}`.
- HTMX detection via `django_htmx` middleware (`request.htmx`).

#### HTMX input
- When using htmx input for autoupdating search/other triggers triggers do not use changed in the hx-trigger. Using input is sufficient.
e.g. instead of
  hx-trigger="input changed delay:300ms, focus"
use
  hx-trigger="input delay:300ms, focus"

### API
- Django Ninja (not DRF). Routers in `{app}/api.py`, registered in `rad/api.py` at `/api/v1/`.
- Use `ModelSchema` with explicit `Meta.fields` lists (avoid `"__all__"` on public endpoints).

## Models

- **Auto-timestamp**: use `created_at = models.DateTimeField(auto_now_add=True)` — match the convention already on the model you are editing.
- **Primary keys**: default Django `AutoField` (integer). Do not switch to UUID without discussion.
- **Audit trail**: wrap models that need history with `@reversion.register()`.
- **Base classes** in `generic/models.py`:
  - `QuestionBase`, `ExamBase`, `UserAnswerBase` — exam content
  - `AuthorMixin` — M2M authors field
  - `CidUser`, `CidUserGroup` — cohort-based access
- After any model change: run `python manage.py makemigrations && python manage.py migrate`.

## Views

- Mix of FBVs and CBVs. Prefer the pattern already used in the app you are editing.
- **Login**: `@login_required` (FBV) or `LoginRequiredMixin` (CBV).
- **Permission decorators** in `generic/decorators.py`: `@user_is_cid_user_manager`, `@check_user_in_group`.
- **Key CBV mixins** in `generic/views.py`: `CidManagerRequiredMixin`, `AuthorRequiredMixin`, `CheckCanEditMixin`.
- Do not expose `can_edit=True` to candidate-facing views. Management and candidate views are intentionally separate.
- When editing views please update docstrings to include their scope and functionality.

## Templates

- **Inheritance**: all full pages use `{% extends "base.html" %}`.
- **Partials path**: `{app}/templates/{app}/partials/_fragment_name.html` (prefix with `_`).
- **Template tags**: always load `{% load static %}`, `{% load django_htmx %}`, `{% load crispy_forms_tags %}` as needed at the top of the file.
- **Bootstrap 5** (dark theme). Use existing utility classes; do not add inline styles.
- To include a partial: `{% include 'app/partials/_fragment.html' %}`. Pass context explicitly when using `with` keyword.
- **Multi line comment** NEVER use {# ... #} style comments for multiline comments as it will render into the html, it is designed for single line comments only. Use 
{% comment "Optional note: You can add a label here if you want" %}
    This is a multiline comment.
    None of this text or any {% tags %} inside here 
    will be rendered or visible in the browser's "View Source".
{% endcomment %}
Instead

## Forms

- **Crispy Forms** with `crispy_bootstrap5`. All forms should use a `FormHelper`.
- `Layout`, `Fieldset`, `Div`, `Row`, `Column` from `crispy_forms.layout` for structure.
- Custom widgets in `generic/widgets.py` — check before creating new widgets.
- Autocomplete fields use `django-autocomplete-light` (`dal`).

## Testing

- **Runner**: `pytest` (config in `pytest.ini`; `--nomigrations` is on by default).
- **Files**: `{app}/tests/test_*.py` or `{app}/tests.py`.
- **Fixtures**: shared fixtures in `{app}/tests/conftest.py`. Check `generic/tests/conftest.py` for common fixtures (`create_user`, `create_examination`, etc.).
- Do not use `TestCase` unless necessary — prefer plain `pytest` functions with `@pytest.mark.django_db`.
- Run tests: `pytest` or `pytest {app}/tests/`.

## Exceptions
- Avoid unnecessary try: except blocks.
- We should not generate empty excepty blocks, if there is an error or bug this should be surfaced (even if just to the logs).

## Celery / Tasks

- Tasks autodiscovered from `{app}/tasks.py`.
- Broker and result backend: Redis at `redis://redis:6379` (Docker service name).
- Import the shared Celery app: `from rad.celery import app as celery_app`.

## Static & Media

- Static: `python manage.py collectstatic --noinput`. App static dirs at `{app}/static/`.
- Media: custom `HashedFilenameFileSystemStorage`. Do not assume filenames are original.
- DICOM thumbnails via `easy_thumbnails` + `helpers.images.pil_dicom_image`.

## Settings & Secrets

- **Never hardcode secrets**. Use `settings_local.py` (gitignored) or environment variables.
- Env vars: `SECRET_KEY`, `DB_NAME/USER/PASSWORD/HOST/PORT`, `MEMCACHE_HOST`, `DEBUG`.
- Local overrides: `rad/settings_local.py` (imported at the bottom of `settings.py`).

## Docker (Dev)

### Start Dev Environment
Start the development server with local configurations:
```bash
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml up -d --build
```

### Run Django Management Commands inside the Container
Since PostgreSQL and Redis run in docker, run all database/management commands within the web container:
```bash
# Run database migrations
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py migrate

# Generate new migrations
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py makemigrations

# Check if migrations are missing (useful for CI/pre-commit)
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py makemigrations --check

# Run system checks
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py check

# Create a Django superuser
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py createsuperuser
```

### Runserver and service commands
To restart the web server/runserver or tail the container output:
```bash
# Restart the runserver/web service container
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml restart web

# View container logs (interactive)
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml logs -f web
```

Dev server runs on `http://localhost:8080` (nginx → gunicorn/runserver).

## Key Conventions

| Convention | Detail |
|---|---|
| HTMX partial detection | `if request.htmx: return render(...)` |
| Partial naming | `_fragment_name.html` with leading underscore |
| Permission checks | Decorators/mixins from `generic/`; never inline `if user.is_staff` in templates |
| Model timestamps | `created_at` (auto_now_add) preferred |
| API schemas | Django Ninja `ModelSchema`; explicit field lists |
| Audit trail | `@reversion.register()` on audited models |
| Logging | `loguru` (`from loguru import logger`) |
| Candidate vs management views | Keep separate; `can_edit` must not leak to candidate views |

## Before Opening a PR

1. `pytest` — all tests pass
2. `python manage.py makemigrations --check` — no missing migrations
3. `python manage.py check` — no system check errors
4. No hardcoded secrets or debug prints
