Metadata-Version: 2.4
Name: django-test-doctor
Version: 0.10.4
Summary: 360° health check for Django projects — URLs, templates, views auth, forms (blank + POST smoke + orphan cleaned_data), admin, models, security, migrations, DRF serializers, unsafe auto-gen parsing, save(update_fields=...) consistency, FK-redundant columns + HTML reporter + --diff mode — one command
Project-URL: Homepage, https://github.com/Altius-Academy-SNC/django-test-doctor
Project-URL: Documentation, https://altius-academy-snc.github.io/django-test-doctor
Project-URL: Repository, https://github.com/Altius-Academy-SNC/django-test-doctor
Project-URL: Issues, https://github.com/Altius-Academy-SNC/django-test-doctor/issues
Author-email: Paul Guindo <paulguindo@altius-group.ch>
License-Expression: MIT
License-File: LICENSE
Keywords: audit,django,healthcheck,linter,quality,smoke-test
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: django>=4.2
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: all
Requires-Dist: coverage>=7.0; extra == 'all'
Requires-Dist: djangorestframework>=3.14; extra == 'all'
Requires-Dist: jinja2>=3.1; extra == 'all'
Provides-Extra: coverage
Requires-Dist: coverage>=7.0; extra == 'coverage'
Provides-Extra: dev
Requires-Dist: coverage>=7.0; extra == 'dev'
Requires-Dist: djangorestframework>=3.14; extra == 'dev'
Requires-Dist: jinja2>=3.1; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-django; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.14; extra == 'drf'
Provides-Extra: html
Requires-Dist: jinja2>=3.1; extra == 'html'
Description-Content-Type: text/markdown

# django-test-doctor

[![PyPI version](https://img.shields.io/pypi/v/django-test-doctor?color=teal&label=PyPI)](https://pypi.org/project/django-test-doctor/)
[![Checks](https://img.shields.io/badge/checks-15-teal)](https://altius-academy-snc.github.io/django-test-doctor/checks/)
[![Tests](https://img.shields.io/badge/tests-72%20passing-brightgreen)](https://github.com/Altius-Academy-SNC/django-test-doctor/tree/main/tests)
[![Python](https://img.shields.io/pypi/pyversions/django-test-doctor?color=blue)](https://pypi.org/project/django-test-doctor/)
[![Django](https://img.shields.io/badge/django-4.2%20%7C%205.x%20%7C%206.0-green)](https://www.djangoproject.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docs](https://img.shields.io/badge/docs-mkdocs--material-teal)](https://altius-academy-snc.github.io/django-test-doctor)

**360° health check for Django projects.** One command, fifteen layers of
analysis, one verdict. Catches crashes your unit tests miss because they
never exercise the blank-init / GET / POST / signal-save / `{% url %}` /
permission-guard / auto-gen-parse / update_fields-consistency /
fk-redundancy pathway an actual user hits first.

Installed as `django-test-doctor`, imported as `django_doctor`:

```bash
pip install django-test-doctor
./manage.py doctor
```

## Why

`./manage.py check` validates runtime configuration. `djlint` validates
template syntax. `pytest` validates whatever you remembered to test.
**Nothing validates the whole project at once.** Doctor does.

Real-world bugs doctor caught in the past week:

- A form whose `__init__` assigns to `self.fields['fonction'].choices`
  crashed with `AttributeError` on every GET because `Contact.fonction` is a
  plain `CharField` — 500 in production. Fixed by the `forms` check.
- A `post_save` signal on `Mandat` created a `ConfigurationTVA` without
  passing the NOT-NULL `regime` FK — `IntegrityError` on every mandat
  creation. Fixed by the `post_smoke` check.
- An `OperationTVAForm.clean()` that assigned `cleaned_data["montant_ttc"]`
  while `montant_ttc` wasn't in `Meta.fields` — Django silently dropped the
  value, NOT NULL violation at save. Fixed by the `forms_meta` check.

Every one of these passed `./manage.py check`, passed its unit test suite,
and still reached production.

## Layers

| Layer           | What it does                                                                    | Status |
|-----------------|---------------------------------------------------------------------------------|--------|
| `system`        | Wraps Django's built-in `check` framework                                       | ✅ 0.1 |
| `urls`          | Crawls `urlpatterns`, probes every URL as anon / user / staff / superuser       | ✅ 0.1 |
| `forms`         | Instantiates every `Form` / `ModelForm` blank + with empty data                 | ✅ 0.1 |
| `migrations`    | Byte-for-byte parity with `makemigrations --check --dry-run`                    | ✅ 0.1 |
| `admin`         | `list_display` / `list_filter` / `search_fields` / `ordering` exist on the model | ✅ 0.2 |
| `models`        | `__str__` defined, `Meta.ordering`, no `SET_NULL` on `NOT NULL` FK              | ✅ 0.2 |
| `security`      | `check --deploy` + weak `SECRET_KEY` + `DEBUG=True` + empty `ALLOWED_HOSTS`     | ✅ 0.2 |
| `drf`           | Serializer init + `Meta.fields` ↔ `Meta.model` coherence                        | ✅ 0.3 |
| `post_smoke`    | POST every `CreateView` / `UpdateView` with auto fixture, fail on 5xx           | ✅ 0.4 |
| `forms_meta`    | `cleaned_data[x] = …` where `x` is NOT NULL but not in `Meta.fields`            | ✅ 0.5 |
| `templates`     | Broken `{% url %}` / `{% static %}` / template syntax in first-party templates  | ✅ 0.6 |
| `views`         | First-party view with no `LoginRequired` / `@login_required` / DRF permission   | ✅ 0.6 |
| `autogen`       | `int(x.split('-')[-1])` auto-gen in `Model.save()` without try/except           | ✅ 0.7 |
| `update_fields` | `obj.save(update_fields=[...])` dropping fields that `save()` recomputes        | ✅ 0.8 |
| `fk_redundant`  | Column duplicating a value reachable via a ForeignKey on the same model         | ✅ 0.10 |
| `i18n`          | Untranslated `msgid`, stale `.mo` vs `.po`, missing `_()` wrapping              | 🚧 0.11 |

Plus in 0.6: `--html <path>` (standalone HTML report suitable for CI
artifacts), `--diff <ref>` (only show findings on files changed since a
git ref — perfect for PR-scoped gating), and a URL-kwargs fixture
generator that unlocks the `urls` and `post_smoke` checks on detail and
edit routes (`<int:pk>`, `<uuid:id>`, `<slug:slug>`).

## Quickstart

```bash
pip install django-test-doctor
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...,
    "django_doctor",
]
```

Then:

```bash
./manage.py doctor                              # full report
./manage.py doctor --section forms,post_smoke   # target specific layers
./manage.py doctor --quick                      # skip slow layers
./manage.py doctor --ci                         # exit 1 on critical/error
./manage.py doctor --json                       # machine-readable output
```

### Safety

`post_smoke` writes to the database (creates a transient superuser, submits
forms) but **always** rolls back via `transaction.atomic()` +
`savepoint_rollback`. Still, point it at a dev or CI database — never
production.

## Configuration

In your project's `pyproject.toml`:

```toml
[tool.django-doctor]
enabled = ["*"]
disabled = []                          # or ["post_smoke"] to skip slow layers
fail_on = ["critical", "error"]        # what --ci should bail on
ignore = ["urls:admin:*", "forms:legacy_migrations.*"]

[tool.django-doctor.urls]
roles = ["anonymous", "authenticated", "staff", "superuser"]
skip  = ["admin:*", "djdt:*", "rp-initiated-logout"]
timeout = 10

[tool.django-doctor.forms]
scenarios = ["blank", "empty_data"]

[tool.django-doctor.migrations]
skip_apps = []
include_third_party = false

[tool.django-doctor.post_smoke]
skip = ["legacy:*"]

[tool.django-doctor.security]
forbidden_secret_keys = ["django-insecure", "changeme", "secret"]
```

## Plugin API

Third-party checks plug in via the `django_doctor.checks` entry point:

```toml
[project.entry-points."django_doctor.checks"]
my_check = "my_package.checks:MyCheck"
```

```python
# my_package/checks.py
# Distribution is django-test-doctor, import path is django_doctor.
from django_doctor import Check, Finding, Severity

class MyCheck(Check):
    id = "my_check"
    description = "Verify my custom invariant"

    def run(self, project):
        for thing in project.iter_something():
            if not thing.ok:
                yield Finding(
                    severity=Severity.ERROR,
                    location=thing.file_path,
                    message=f"{thing!r} is broken",
                    fix_hint="Run ./manage.py fix_things",
                )
```

Need auto-generated form fixtures in your own tests? The helper doctor uses
internally is public:

```python
from django_doctor.fixtures import make_form_fixture

data = make_form_fixture(MyModelForm)
client.post("/my-view/", data)
```

## Status

Alpha — 0.10 ships fifteen layers + HTML reporter + --diff mode + URL-kwargs fixtures. Roadmap in `CHANGELOG.md`. Battle-tested
against a production Django 6 / DRF / PostGIS / pgvector codebase with ~175
models, ~150 forms, 202 serializers, and 300+ URLs.

## License

MIT © Paul Guindo
